From ff7119ec542dfd2d0b6ed4ed37d2e1b16c3916ec Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 17 Nov 2014 12:28:11 -0600 Subject: [PATCH 0001/1059] RestDocumentationResultActions->RestDocumentationResultHandler Previously documentation was handled by using a ResultActions implementation that had custom methods added to it. Now documentation is handled using a ResultHandler. This has a few advantages: - Fit better with the MockMvc programming model. This is similar to andDo(print()) - Support for using alwaysDo --- .../com/example/notes/ApiDocumentation.java | 119 +++++++++-------- .../notes/GettingStartedDocumentation.java | 121 +++++++++--------- .../com/example/notes/ApiDocumentation.java | 117 ++++++++--------- .../notes/GettingStartedDocumentation.java | 118 +++++++++-------- .../restdocs/core/RestDocumentation.java | 40 +++--- .../core/RestDocumentationResultActions.java | 63 --------- .../core/RestDocumentationResultHandler.java | 52 ++++++++ 7 files changed, 297 insertions(+), 333 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultActions.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java diff --git a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java index 2c99a395e..e3d264189 100644 --- a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java @@ -19,8 +19,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.restdocs.core.RestDocumentation.halLinks; +import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; 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; @@ -76,34 +76,33 @@ public void setUp() { @Test public void errorExample() throws Exception { - document( - "error-example", - this.mockMvc - .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist"))) + this.mockMvc + .perform(get("/error") + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, + "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, + "The tag 'http://localhost:8080/tags/123' does not exist")) .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath("error", is("Bad Request"))) .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))); + .andExpect(jsonPath("path", is(notNullValue()))) + .andDo(document("error-example")); } @Test public void indexExample() throws Exception { - document("index-example", - this.mockMvc.perform(get("/")).andExpect(status().isOk())) - .andDocumentLinks( - halLinks(), + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index-example").withLinks(halLinks(), linkWithRel("notes").description( "The <>"), linkWithRel("tags").description( "The <>"), linkWithRel("profile").description( - "The ALPS profile for the service")); + "The ALPS profile for the service"))); + } @Test @@ -116,8 +115,9 @@ public void notesListExample() throws Exception { "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - document("notes-list-example", this.mockMvc.perform(get("/notes"))).andExpect( - status().isOk()); + this.mockMvc.perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(document("notes-list-example")); } @Test @@ -137,12 +137,11 @@ public void notesCreateExample() throws Exception { note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); note.put("tags", Arrays.asList(tagLocation)); - document( - "notes-create-example", - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))).andExpect( - status().isCreated())); + this.mockMvc.perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))).andExpect( + status().isCreated()) + .andDo(document("notes-create-example")); } @Test @@ -169,17 +168,17 @@ public void noteGetExample() throws Exception { .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); - document("note-get-example", this.mockMvc.perform(get(noteLocation))) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDocumentLinks( - halLinks(), - linkWithRel("self").description("This <>"), - linkWithRel("tags").description( - "This note's <>")); + this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("note-get-example") + .withLinks(halLinks(), + linkWithRel("self").description("This <>"), + linkWithRel("tags").description( + "This note's <>"))); } @@ -192,8 +191,9 @@ public void tagsListExample() throws Exception { createTag("Hypermedia"); createTag("HTTP"); - document("tags-list-example", this.mockMvc.perform(get("/tags"))).andExpect( - status().isOk()); + this.mockMvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(document("tags-list-example")); } @Test @@ -201,12 +201,11 @@ public void tagsCreateExample() throws Exception { Map tag = new HashMap(); tag.put("name", "REST"); - document( - "tags-create-example", - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))).andExpect( - status().isCreated())); + this.mockMvc.perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))).andExpect( + status().isCreated()) + .andDo(document("tags-create-example")); } @Test @@ -241,12 +240,11 @@ public void noteUpdateExample() throws Exception { Map noteUpdate = new HashMap(); noteUpdate.put("tags", Arrays.asList(tagLocation)); - document( - "note-update-example", - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent())); + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("note-update-example")); } @Test @@ -261,16 +259,14 @@ public void tagGetExample() throws Exception { .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); - document("tag-get-example", this.mockMvc.perform(get(tagLocation))) - .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDocumentLinks( - halLinks(), + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(document("tag-get-example").withLinks(halLinks(), linkWithRel("self").description("This <>"), linkWithRel("notes") .description( - "The <> that have this tag")); - + "The <> that have this tag"))); } @Test @@ -288,12 +284,11 @@ public void tagUpdateExample() throws Exception { Map tagUpdate = new HashMap(); tagUpdate.put("name", "RESTful"); - document( - "tag-update-example", - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent())); + this.mockMvc.perform( + patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("tag-update-example")); } private void createNote(String title, String body) { diff --git a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java index fe7e0e65c..8687ff2ad 100644 --- a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java @@ -71,12 +71,11 @@ public void setUp() { @Test public void index() throws Exception { - document( - "index", - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue())))); + this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("index")); } @Test @@ -101,49 +100,47 @@ String createNote() throws Exception { note.put("title", "Note creation with cURL"); note.put("body", "An example of how to create a note using cURL"); - String noteLocation = document( - "create-note", - this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getNote(String noteLocation) throws Exception { - document( - "get-note", - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue())))); + this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("get-note")); } String createTag() throws Exception, JsonProcessingException { Map tag = new HashMap(); tag.put("name", "getting-started"); - String tagLocation = document( - "create-tag", - this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) - .andReturn().getResponse().getHeader("Location"); + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-tag")) + .andReturn().getResponse().getHeader("Location"); return tagLocation; } void getTag(String tagLocation) throws Exception { - document( - "get-tag", - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.notes", is(notNullValue())))); + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(notNullValue()))) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andDo(document("get-tag")); } String createTaggedNote(String tag) throws Exception { @@ -152,59 +149,61 @@ String createTaggedNote(String tag) throws Exception { note.put("body", "An example of how to create a tagged note using cURL"); note.put("tags", Arrays.asList(tag)); - String noteLocation = document( - "create-tagged-note", - this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getTaggedNote(String tagLocation) throws Exception { - document( - "get-tagged-note", - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue())))); + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("get-tagged-note")); } void getTags(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - document("get-tags", - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1)))); + + this.mockMvc.perform(get(tagsLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1))) + .andDo(document("get-tags")); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { Map update = new HashMap(); update.put("tags", Arrays.asList(tagLocation)); - document( - "tag-existing-note", - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(update))).andExpect( - status().isNoContent())); + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(update))) + .andExpect(status().isNoContent()) + .andDo(document("tag-existing-note")); } void getTaggedExistingNote(String tagLocation) throws Exception { - document("get-tagged-existing-note", this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk())); + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andDo(document("get-tagged-existing-note")); } void getTagsForExistingNote(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - document("get-tags-for-existing-note", - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1)))); + this.mockMvc.perform(get(tagsLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1))) + .andDo(document("get-tags-for-existing-note")); } private String getLink(MvcResult result, String href) diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java index dfa120509..745c4bfe3 100644 --- a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java @@ -76,32 +76,30 @@ public void setUp() { @Test public void errorExample() throws Exception { - document( - "error-example", - this.mockMvc - .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist"))) - .andDo(print()).andExpect(status().isBadRequest()) - .andExpect(jsonPath("error", is("Bad Request"))) - .andExpect(jsonPath("timestamp", is(notNullValue()))) - .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))); + this.mockMvc + .perform(get("/error") + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, + "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, + "The tag 'http://localhost:8080/tags/123' does not exist")) + .andDo(print()).andExpect(status().isBadRequest()) + .andExpect(jsonPath("error", is("Bad Request"))) + .andExpect(jsonPath("timestamp", is(notNullValue()))) + .andExpect(jsonPath("status", is(400))) + .andExpect(jsonPath("path", is(notNullValue()))) + .andDo(document("error-example")); } @Test public void indexExample() throws Exception { - document("index-example", - this.mockMvc.perform(get("/")) - .andExpect(status().isOk())) - .andDocumentLinks(halLinks(), + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index-example").withLinks(halLinks(), linkWithRel("notes").description( "The <>"), linkWithRel("tags").description( - "The <>")); + "The <>"))); } @Test @@ -114,8 +112,9 @@ public void notesListExample() throws Exception { "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - document("notes-list-example", this.mockMvc.perform(get("/notes"))).andExpect( - status().isOk()); + this.mockMvc.perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(document("notes-list-example")); } @Test @@ -135,12 +134,11 @@ public void notesCreateExample() throws Exception { note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); note.put("tags", Arrays.asList(tagLocation)); - document( - "notes-create-example", - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))).andExpect( - status().isCreated())); + this.mockMvc.perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andDo(document("notes-create-example")); } @Test @@ -167,16 +165,16 @@ public void noteGetExample() throws Exception { .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); - document("note-get-example", this.mockMvc.perform(get(noteLocation))) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDocumentLinks(halLinks(), + this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .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(halLinks(), linkWithRel("self").description("This <>"), linkWithRel("note-tags").description( - "This note's <>")); + "This note's <>"))); } @@ -189,8 +187,9 @@ public void tagsListExample() throws Exception { createTag("Hypermedia"); createTag("HTTP"); - document("tags-list-example", this.mockMvc.perform(get("/tags"))).andExpect( - status().isOk()); + this.mockMvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(document("tags-list-example")); } @Test @@ -198,12 +197,11 @@ public void tagsCreateExample() throws Exception { Map tag = new HashMap(); tag.put("name", "REST"); - document( - "tags-create-example", - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))).andExpect( - status().isCreated())); + this.mockMvc.perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andDo(document("tags-create-example")); } @Test @@ -238,12 +236,11 @@ public void noteUpdateExample() throws Exception { Map noteUpdate = new HashMap(); noteUpdate.put("tags", Arrays.asList(tagLocation)); - document( - "note-update-example", - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent())); + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("note-update-example")); } @Test @@ -258,15 +255,14 @@ public void tagGetExample() throws Exception { .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); - document("tag-get-example", this.mockMvc.perform(get(tagLocation))) - .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDocumentLinks(halLinks(), + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(document("tag-get-example").withLinks(halLinks(), linkWithRel("self").description("This <>"), linkWithRel("tagged-notes") .description( - "The <> that have this tag")); - + "The <> that have this tag"))); } @Test @@ -284,12 +280,11 @@ public void tagUpdateExample() throws Exception { Map tagUpdate = new HashMap(); tagUpdate.put("name", "RESTful"); - document( - "tag-update-example", - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent())); + this.mockMvc.perform( + patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("tag-update-example")); } private void createNote(String title, String body) { diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java index 437cfb97d..9c57ede92 100644 --- a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java @@ -71,12 +71,11 @@ public void setUp() { @Test public void index() throws Exception { - document( - "index", - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue())))); + this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("index")); } @Test @@ -101,49 +100,45 @@ String createNote() throws Exception { note.put("title", "Note creation with cURL"); note.put("body", "An example of how to create a note using cURL"); - String noteLocation = document( - "create-note", - this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getNote(String noteLocation) throws Exception { - document( - "get-note", - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue())))); + this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) + .andDo(document("get-note")); } String createTag() throws Exception, JsonProcessingException { Map tag = new HashMap(); tag.put("name", "getting-started"); - String tagLocation = document( - "create-tag", - this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-tag")) .andReturn().getResponse().getHeader("Location"); return tagLocation; } void getTag(String tagLocation) throws Exception { - document( - "get-tag", - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.tagged-notes", is(notNullValue())))); + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("name", is(notNullValue()))) + .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))) + .andDo(document("get-tag")); } String createTaggedNote(String tag) throws Exception { @@ -152,59 +147,60 @@ String createTaggedNote(String tag) throws Exception { note.put("body", "An example of how to create a tagged note using cURL"); note.put("tags", Arrays.asList(tag)); - String noteLocation = document( - "create-tagged-note", - this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue()))) + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getTaggedNote(String tagLocation) throws Exception { - document( - "get-tagged-note", - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue())))); + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) + .andDo(document("get-tagged-note")); } void getTags(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "note-tags"); - document("get-tags", - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1)))); + this.mockMvc.perform(get(tagsLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1))) + .andDo(document("get-tags")); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { Map update = new HashMap(); update.put("tags", Arrays.asList(tagLocation)); - document( - "tag-existing-note", - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(update))).andExpect( - status().isNoContent())); + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(update))) + .andExpect(status().isNoContent()) + .andDo(document("tag-existing-note")); } void getTaggedExistingNote(String tagLocation) throws Exception { - document("get-tagged-existing-note", this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk())); + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andDo(document("get-tagged-existing-note")); } void getTagsForExistingNote(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "note-tags"); - document("get-tags-for-existing-note", - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1)))); + this.mockMvc.perform(get(tagsLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1))) + .andDo(document("get-tags-for-existing-note")); } private String getLink(MvcResult result, String rel) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java index effbf6a0f..2aa9865ac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java @@ -16,36 +16,26 @@ package org.springframework.restdocs.core; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse; - import java.util.Map; -import org.springframework.test.web.servlet.ResultActions; - public class RestDocumentation { - public static RestDocumentationResultActions document(String outputDir, - ResultActions resultActions) throws Exception { - return new RestDocumentationResultActions(outputDir, resultActions) - .andDo(documentCurlRequest(outputDir).includeResponseHeaders()) - .andDo(documentCurlResponse(outputDir).includeResponseHeaders()) - .andDo(documentCurlRequestAndResponse(outputDir).includeResponseHeaders()); - } + public static RestDocumentationResultHandler document(String outputDir) throws Exception { + return new RestDocumentationResultHandler(outputDir); + } - public static LinkDescriptor linkWithRel(String rel) { - return new LinkDescriptor(rel); - } + public static LinkDescriptor linkWithRel(String rel) { + return new LinkDescriptor(rel); + } - public static LinkExtractor halLinks() { - return new LinkExtractor() { + public static LinkExtractor halLinks() { + return new LinkExtractor() { - @SuppressWarnings("unchecked") - @Override - public Map extractLinks(Map responseJson) { - return (Map) responseJson.get("_links"); - } - }; - } + @SuppressWarnings("unchecked") + @Override + public Map extractLinks(Map responseJson) { + return (Map) responseJson.get("_links"); + } + }; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultActions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultActions.java deleted file mode 100644 index 670a32e7b..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultActions.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2014 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.core; - -import java.util.Arrays; - -import org.springframework.restdocs.core.RestDocumentationResultHandlers.LinkDocumentingResultHandler; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.ResultHandler; -import org.springframework.test.web.servlet.ResultMatcher; - -public class RestDocumentationResultActions implements ResultActions { - - private final ResultActions delegate; - - private final String outputDir; - - public RestDocumentationResultActions(String outputDir, ResultActions delegate) { - this.outputDir = outputDir; - this.delegate = delegate; - } - - @Override - public RestDocumentationResultActions andExpect(ResultMatcher matcher) - throws Exception { - this.delegate.andExpect(matcher); - return this; - } - - @Override - public RestDocumentationResultActions andDo(ResultHandler handler) throws Exception { - this.delegate.andDo(handler); - return this; - } - - @Override - public MvcResult andReturn() { - return this.delegate.andReturn(); - } - - public RestDocumentationResultActions andDocumentLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) - throws Exception { - this.delegate.andDo(new LinkDocumentingResultHandler(this.outputDir, linkExtractor, Arrays - .asList(descriptors))); - return this; - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java new file mode 100644 index 000000000..1e00cf4d9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 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.core; + +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest; +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse; +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse; + +import java.util.Arrays; + +import org.springframework.restdocs.core.RestDocumentationResultHandlers.LinkDocumentingResultHandler; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; + +public class RestDocumentationResultHandler implements ResultHandler { + + private final String outputDir; + private ResultHandler linkDocumentingResultHandler; + + public RestDocumentationResultHandler(String outputDir) { + this.outputDir = outputDir; + } + + @Override + public void handle(MvcResult result) throws Exception { + documentCurlRequest(outputDir).includeResponseHeaders().handle(result); + documentCurlResponse(outputDir).includeResponseHeaders().handle(result); + documentCurlRequestAndResponse(outputDir).includeResponseHeaders().handle(result); + if(linkDocumentingResultHandler != null) { + linkDocumentingResultHandler.handle(result); + } + } + + public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { + linkDocumentingResultHandler = new LinkDocumentingResultHandler(outputDir, linkExtractor, Arrays.asList(descriptors)); + return this; + } +} From 23e8ad3daedee76dd2ad1d922ccc132b9684c92c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Dec 2014 12:20:08 +0000 Subject: [PATCH 0002/1059] Update the README to reflect the changes to how document is used --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e10182991..5cfd9e242 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,14 @@ public void setUp() { The default values are `http`, `localhost`, and `8080`. You can omit the above configuration if these defaults meet your needs. -To document a MockMvc call, wrap it in a call to `RestDocumentation.document`: +To document a MockMvc call, you use MockMvc's `andDo` method, passing it a +`RestDocumentationResultHandler` that can be easily obtained from +the static `RestDocumentation.document` method: ```java public void getIndex() { - document("index", this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))); + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andDo(document("index")); } ``` From 6ec522069a2dca8c5ae95a11839afe8636ca09db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2015 16:51:47 +0000 Subject: [PATCH 0003/1059] Make things less Gradle-specific and provide Maven config in samples Previously, the project provided a Gradle plugin and was only really intended for use with Gradle. This had influenced a number of design choices that have proven to be less than ideal when attempting to use the project with Maven. This commit backs off from a number of design decisions that worked well with Gradle but less so with Maven. Part of this is that the Gradle plugin is (temporarily?) no more. It's been removed in favour of configuring things directly in build.gradle. While this slightly increases the amount of configuration required it makes things far more flexible. Both samples have been updated with modified Gradle configuration and new Maven configuration. The README has also been updated to reflect these changes. --- .gitignore | 3 +- README.md | 212 ++++++++++--- build.gradle | 9 - rest-notes-spring-data-rest/build.gradle | 39 ++- rest-notes-spring-data-rest/pom.xml | 99 ++++++ .../resources/documentation.properties | 1 - .../asciidoc/api-guide.asciidoc | 0 .../asciidoc/getting-started-guide.asciidoc | 0 .../com/example/notes/ApiDocumentation.java | 0 .../notes/GettingStartedDocumentation.java | 0 rest-notes-spring-hateoas/build.gradle | 39 ++- rest-notes-spring-hateoas/pom.xml | 115 +++++++ .../resources/documentation.properties | 1 - .../asciidoc/api-guide.asciidoc | 0 .../asciidoc/getting-started-guide.asciidoc | 0 .../com/example/notes/ApiDocumentation.java | 0 .../notes/GettingStartedDocumentation.java | 2 + settings.gradle | 3 +- .../core/DocumentationProperties.java | 13 +- .../core/RestDocumentationResultHandlers.java | 17 +- .../.settings/org.eclipse.jdt.core.prefs | 295 ------------------ .../org.eclipse.jdt.groovy.core.prefs | 2 - .../.settings/org.eclipse.jdt.ui.prefs | 62 ---- .../gradle/RestDocumentationPlugin.groovy | 75 ----- .../gradle/RestDocumentationSnippets.groovy | 30 -- .../org.springframework.restdocs.properties | 1 - 26 files changed, 462 insertions(+), 556 deletions(-) create mode 100644 rest-notes-spring-data-rest/pom.xml delete mode 100644 rest-notes-spring-data-rest/src/documentation/resources/documentation.properties rename rest-notes-spring-data-rest/src/{documentation => main}/asciidoc/api-guide.asciidoc (100%) rename rest-notes-spring-data-rest/src/{documentation => main}/asciidoc/getting-started-guide.asciidoc (100%) rename rest-notes-spring-data-rest/src/{documentation => test}/java/com/example/notes/ApiDocumentation.java (100%) rename rest-notes-spring-data-rest/src/{documentation => test}/java/com/example/notes/GettingStartedDocumentation.java (100%) create mode 100644 rest-notes-spring-hateoas/pom.xml delete mode 100644 rest-notes-spring-hateoas/src/documentation/resources/documentation.properties rename rest-notes-spring-hateoas/src/{documentation => main}/asciidoc/api-guide.asciidoc (100%) rename rest-notes-spring-hateoas/src/{documentation => main}/asciidoc/getting-started-guide.asciidoc (100%) rename rest-notes-spring-hateoas/src/{documentation => test}/java/com/example/notes/ApiDocumentation.java (100%) rename rest-notes-spring-hateoas/src/{documentation => test}/java/com/example/notes/GettingStartedDocumentation.java (98%) delete mode 100644 spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.core.prefs delete mode 100644 spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.groovy.core.prefs delete mode 100644 spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.ui.prefs delete mode 100644 spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationPlugin.groovy delete mode 100644 spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationSnippets.groovy delete mode 100644 spring-restdocs-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.restdocs.properties diff --git a/.gitignore b/.gitignore index f8506e43e..91fa34bad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .project .settings bin -build \ No newline at end of file +build +target \ No newline at end of file diff --git a/README.md b/README.md index 5cfd9e242..328378e99 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ ## Goal The primary goal of the project is to make it easy to document RESTful services by -combining content that's been hand-written using [AsciiDoctor][1] with auto-generated -examples produced with the [Spring MVC Test][2] framework. The result is intended to -be an easy-to-read user guide, akin to [GitHub's API documentation][3] for example, -rather than the fully automated, dense API documentation produced by tools like -[Swagger][4]. +combining content that's been hand-written with auto-generated examples produced +with the [Spring MVC Test][2] framework. The result is intended to be an easy-to-read +user guide, akin to [GitHub's API documentation][3] for example, rather than the fully +automated, dense API documentation produced by tools like [Swagger][4]. For a broader introduction see the [Documenting RESTful APIs][9] presentation. @@ -18,59 +17,171 @@ $ ./gradlew build install ``` Once the main project's built, take a look at one of the two sample projects. Both -projects implement a RESTful service for creating tagged notes but have different -implementations: `rest-notes-spring-hateoas` is implemented using Spring MVC and Spring -Hateoas while `rest-notes-spring-data-rest` is implemented using Spring Data REST. +projects implement a RESTful service for creating tagged notes and illustrate the use +of Maven or Gradle. The two projects have different implementations: +`rest-notes-spring-hateoas` is implemented using Spring MVC and Spring Hateoas where as +`rest-notes-spring-data-rest` is implemented using Spring Data REST. -To see the sample project's documentation move into its directory and use Gradle to -build the documentation. For example: +Every example request and response in the documentation is auto-generated using custom +Spring MVC Test result handlers. This ensures that the examples match the service that +they are documenting. + +### Building a sample with Gradle + +To see the sample project's documentation, move into its directory and use Gradle to +build it. For example: ``` $ cd rest-notes-spring-data-rest -$ ./gradlew restDocumentation +$ ./gradlew asciidoctor ``` Once the build is complete, open one of the following: -- build/asciidoc/getting-started-guide.html -- build/asciidoc/api-guide.html +- build/asciidoc/html5/getting-started-guide.html +- build/asciidoc/html5/api-guide.html -Every example request and response in the documentation is auto-generated using custom -Spring MVC Test result handlers. This ensures that the examples match the service that -they are documenting. +### Building a sample with Maven + +To see the sample project's documentation, move into its directory and use Maven to build +it. For example: + +``` +$ cd rest-notes-spring-hateoas +$ mvn package +``` + +Once the build is complete, open one of the following: + +- target/generated-docs/getting-started-guide.html +- target/generated-docs/api-guide.html -## How does it work +## How it works There are three main pieces involved in using this project to document your RESTful service. -### Gradle plugin +### Build configuration + +Both Maven and Gradle are supported. + +#### Gradle configuration + +You can look at either samples' `build.gradle` file to see the required configuration. +The key parts are described below. + +Configure the AsciiDoctor plugin: + +```groovy +plugins { + id "org.asciidoctor.convert" version "1.5.2" +} +``` + +Add a dependency on `spring-restdocs-core` in the `testCompile` configuration: + +```groovy +dependencies { + testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' +} +``` + +Configure a property to control the location of the generated snippets: + +```groovy +ext { + generatedDocumentation = file('build/generated-snippets') +} +``` -A Gradle plugin is provided. This plugin builds on top of the [AsciiDoctor plugin][5] -and is responsible for producing the documentation during the build. Assuming you've -built and installed the project as described in the quick start, the plugin as -configured in your project as follows: +Configure the `test` task with a system property to control the location to which the +snippets are generated: + +test { + systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation + outputs.dir generatedDocumentation +} + +Configure the `asciidoctor` task. The `generated` attribute is used to provide easy +access to the generated snippets: ```groovy -buildscript { - repositories { - mavenLocal() - jcenter() - } - dependencies { - classpath 'org.springframework.restdocs:spring-restdocs-gradle-plugin:0.1.0.BUILD-SNAPSHOT' - } +asciidoctor { + attributes 'generated': generatedDocumentation + inputs.dir generatedDocumentation + dependsOn test } -apply plugin: 'org.springframework.restdocs' +``` + +#### Maven configuration + +You can look at either samples' `pom.xml` file to see the required configuration. The key +parts are described below: + +Add a dependency on `spring-restdocs-core` in the `test` scope: + +```xml + + org.springframework.restdocs + spring-restdocs-core + 0.1.0.BUILD-SNAPSHOT + test + +``` + +Configure the SureFire plugin with a system property to control the location to which +the snippets are generated: + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + + + ${project.build.directory}/generated-snippets + + + +``` + +Configure the AsciiDoctor plugin. The `generated` attribute is used to provide easy +access to the generated snippets: + +```xml + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + package + + process-asciidoc + + + html + book + + ${project.build.directory}/generated-snippets + + + + + + ``` ### Programatically generated snippets Spring's MVC Test framework is used to make requests to the service that you are -documenting. Any such request wrapped in a call to `RestDocumentation.document` will -produce individual documentation snippets for its request and its response as well as -a snippet that contains both its request and its response. +documenting. A custom `ResultHandler` is used to produce individual documentation +snippets for its request and its response as well as a snippet that contains both its +request and its response. You can configure the scheme, host, and port of any URIs that appear in the documentation snippets: @@ -105,31 +216,50 @@ public void getIndex() { The code above will perform a `GET` request against the index (`/`) of the service with an accept header indicating that a JSON response is required. It will write the cURL command for the request and the resulting response to files in a directory named -`index` in the project's `build/generated-documentation/` directory. Three files will +`index` in the project's `build/generated-snippets/` directory. Three files will be written: - `index/request.asciidoc` - `index/response.asciidoc` - `index/request-response.asciidoc` -### Documentation written in Asciidoc +### Hand-written documentation Producing high-quality, easily readable documentation is difficult and the process is only made harder by trying to write the documentation in an ill-suited format such as Java annotations. This project addresses this by allowing you to write the bulk of -your documentation's text as an Asciidoc document. These files should be placed in -`src/documentation/asciidoc`. +your documentation's text using [Asciidoctor][1]. The default location for source files +depends on whether you're using Maven or Gradle. By default, AsciiDoctor's Maven plugin +looks in `src/main/asciidoc`, whereas the AsciiDoctor Gradle plugin looks in +`src/docs/asciidoc` To include the programmatically generated snippets in your documentation, you use -Asciidoc's [`include` macro][6]. The Gradle plugin provides an attribute, `generated`, -that you can use to reference the directory to which the snippets are written. For -example, to include both the request and response snippets described above: +Asciidoc's [`include` macro][6]. The Maven and Gradle configuration described above +configures an attribute, `generated`, that you can use to reference the directory to +which the snippets are written. For example, to include both the request and response +snippets described above: ``` include::{generated}/index/request.asciidoc[] include::{generated}/index/response.asciidoc[] ``` +## Generating snippets in your IDE + +As described above, a system property is used to configure the location to which the +generated snippets are written. When running documentation tests in your IDE this system +property will not have been set. If the property is not set the snippets will be written +to standard out. + +If you'd prefer that your IDE writes the snippets to disk you can use a file +in `src/test/resources` named `documentation.properties` to configure the property. +For example: + +```properties +org.springframework.restdocs.outputDir: target/generated-snippets + +``` + ## Learning more To learn more, take a look at the accompanying sample projects: diff --git a/build.gradle b/build.gradle index 55117d1ab..e61cbffe3 100644 --- a/build.gradle +++ b/build.gradle @@ -20,15 +20,6 @@ project(':spring-restdocs-core') { } } -project(':spring-restdocs-gradle-plugin') { - apply plugin: 'groovy' - - dependencies { - compile gradleApi() - runtime 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.0' - } -} - subprojects { repositories { jcenter() diff --git a/rest-notes-spring-data-rest/build.gradle b/rest-notes-spring-data-rest/build.gradle index 95b693ae5..386a81d22 100644 --- a/rest-notes-spring-data-rest/build.gradle +++ b/rest-notes-spring-data-rest/build.gradle @@ -1,26 +1,23 @@ buildscript { repositories { - mavenLocal() - maven { - url 'https://repo.spring.io/libs-milestone' - } + mavenCentral() } dependencies { - classpath 'org.springframework.restdocs:spring-restdocs-gradle-plugin:0.1.0.BUILD-SNAPSHOT' - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.M2' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE' } } +plugins { + id "org.asciidoctor.convert" version "1.5.2" +} + apply plugin: 'java' apply plugin: 'spring-boot' apply plugin: 'eclipse' -apply plugin: 'org.springframework.restdocs' repositories { mavenLocal() - maven { - url 'https://repo.spring.io/libs-milestone' - } + mavenCentral() } group = 'com.example' @@ -31,10 +28,26 @@ dependencies { runtime 'com.h2database:h2' - documentationCompile 'com.jayway.jsonpath:json-path' - documentationCompile 'org.springframework.boot:spring-boot-starter-test' - documentationCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + testCompile 'com.jayway.jsonpath:json-path' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + +} + +ext { + generatedDocumentation = file('build/generated-snippets') +} + +test { + systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation + outputs.dir generatedDocumentation +} +asciidoctor { + sourceDir 'src/main/asciidoc' // Align with Maven's default location + attributes 'generated': generatedDocumentation + inputs.dir generatedDocumentation + dependsOn test } task wrapper(type: Wrapper) { diff --git a/rest-notes-spring-data-rest/pom.xml b/rest-notes-spring-data-rest/pom.xml new file mode 100644 index 000000000..10a33975d --- /dev/null +++ b/rest-notes-spring-data-rest/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + com.example + rest-notes-spring-hateoas + 0.0.1-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 1.2.1.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + org.springframework.restdocs + spring-restdocs-core + 0.1.0.BUILD-SNAPSHOT + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + + + ${project.build.directory}/generated-snippets + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + package + + process-asciidoc + + + html + book + + ${project.build.directory}/generated-snippets + + + + + + + + + \ No newline at end of file diff --git a/rest-notes-spring-data-rest/src/documentation/resources/documentation.properties b/rest-notes-spring-data-rest/src/documentation/resources/documentation.properties deleted file mode 100644 index 901c0049e..000000000 --- a/rest-notes-spring-data-rest/src/documentation/resources/documentation.properties +++ /dev/null @@ -1 +0,0 @@ -org.springframework.restdocs.outputDir = build/generated-documentation \ No newline at end of file diff --git a/rest-notes-spring-data-rest/src/documentation/asciidoc/api-guide.asciidoc b/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc similarity index 100% rename from rest-notes-spring-data-rest/src/documentation/asciidoc/api-guide.asciidoc rename to rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc diff --git a/rest-notes-spring-data-rest/src/documentation/asciidoc/getting-started-guide.asciidoc b/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc similarity index 100% rename from rest-notes-spring-data-rest/src/documentation/asciidoc/getting-started-guide.asciidoc rename to rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc diff --git a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java similarity index 100% rename from rest-notes-spring-data-rest/src/documentation/java/com/example/notes/ApiDocumentation.java rename to rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java diff --git a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java similarity index 100% rename from rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java rename to rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java diff --git a/rest-notes-spring-hateoas/build.gradle b/rest-notes-spring-hateoas/build.gradle index 61ccd7d45..30b85f68a 100644 --- a/rest-notes-spring-hateoas/build.gradle +++ b/rest-notes-spring-hateoas/build.gradle @@ -1,26 +1,23 @@ buildscript { repositories { - mavenLocal() - maven { - url "https://repo.spring.io/libs-milestone" - } + mavenCentral() } dependencies { - classpath 'org.springframework.restdocs:spring-restdocs-gradle-plugin:0.1.0.BUILD-SNAPSHOT' - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.M2' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE' } } +plugins { + id "org.asciidoctor.convert" version "1.5.2" +} + apply plugin: 'java' apply plugin: 'spring-boot' apply plugin: 'eclipse' -apply plugin: 'org.springframework.restdocs' repositories { mavenLocal() - maven { - url "https://repo.spring.io/libs-milestone" - } + mavenCentral() } group = 'com.example' @@ -34,9 +31,25 @@ dependencies { runtime 'org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE' runtime 'org.atteo:evo-inflector:1.2' - documentationCompile 'com.jayway.jsonpath:json-path' - documentationCompile 'org.springframework.boot:spring-boot-starter-test' - documentationCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + testCompile 'com.jayway.jsonpath:json-path' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' +} + +ext { + generatedDocumentation = file('build/generated-snippets') +} + +test { + systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation + outputs.dir generatedDocumentation +} + +asciidoctor { + sourceDir 'src/main/asciidoc' // Align with Maven's default location + attributes 'generated': generatedDocumentation + inputs.dir generatedDocumentation + dependsOn test } task wrapper(type: Wrapper) { diff --git a/rest-notes-spring-hateoas/pom.xml b/rest-notes-spring-hateoas/pom.xml new file mode 100644 index 000000000..3ed34f892 --- /dev/null +++ b/rest-notes-spring-hateoas/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + com.example + rest-notes-spring-hateoas + 0.0.1-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 1.2.1.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.hateoas + spring-hateoas + + + + org.springframework.plugin + spring-plugin-core + 1.1.0.RELEASE + runtime + + + com.h2database + h2 + runtime + + + org.atteo + evo-inflector + 1.2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + org.springframework.restdocs + spring-restdocs-core + 0.1.0.BUILD-SNAPSHOT + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + + + ${project.build.directory}/generated-snippets + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + package + + process-asciidoc + + + html + book + + ${project.build.directory}/generated-snippets + + + + + + + + + \ No newline at end of file diff --git a/rest-notes-spring-hateoas/src/documentation/resources/documentation.properties b/rest-notes-spring-hateoas/src/documentation/resources/documentation.properties deleted file mode 100644 index 901c0049e..000000000 --- a/rest-notes-spring-hateoas/src/documentation/resources/documentation.properties +++ /dev/null @@ -1 +0,0 @@ -org.springframework.restdocs.outputDir = build/generated-documentation \ No newline at end of file diff --git a/rest-notes-spring-hateoas/src/documentation/asciidoc/api-guide.asciidoc b/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc similarity index 100% rename from rest-notes-spring-hateoas/src/documentation/asciidoc/api-guide.asciidoc rename to rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc diff --git a/rest-notes-spring-hateoas/src/documentation/asciidoc/getting-started-guide.asciidoc b/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc similarity index 100% rename from rest-notes-spring-hateoas/src/documentation/asciidoc/getting-started-guide.asciidoc rename to rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java similarity index 100% rename from rest-notes-spring-hateoas/src/documentation/java/com/example/notes/ApiDocumentation.java rename to rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java similarity index 98% rename from rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java rename to rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 9c57ede92..a37e573e1 100644 --- a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -23,6 +23,7 @@ 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; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -172,6 +173,7 @@ void getTags(String taggedNoteLocation) throws Exception { .andReturn(), "note-tags"); this.mockMvc.perform(get(tagsLocation)) .andExpect(status().isOk()) + .andDo(print()) .andExpect(jsonPath("_embedded.tags", hasSize(1))) .andDo(document("get-tags")); } diff --git a/settings.gradle b/settings.gradle index 5b586ab16..c74ec6c26 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,3 @@ rootProject.name = 'spring-restdocs' -include 'spring-restdocs-core' -include 'spring-restdocs-gradle-plugin' \ No newline at end of file +include 'spring-restdocs-core' \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java index ba8c5d18e..8ecc84b5b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java @@ -21,13 +21,13 @@ import java.io.InputStream; import java.util.Properties; +import org.springframework.util.StringUtils; + class DocumentationProperties { private final Properties properties = new Properties(); DocumentationProperties() { - this.properties.putAll(System.getProperties()); - InputStream stream = getClass().getClassLoader().getResourceAsStream( "documentation.properties"); if (stream != null) { @@ -47,11 +47,14 @@ class DocumentationProperties { } } } + this.properties.putAll(System.getProperties()); } File getOutputDir() { - return new File(this.properties.getProperty( - "org.springframework.restdocs.outputDir", "generated-documentation")) - .getAbsoluteFile(); + String outputDir = this.properties.getProperty("org.springframework.restdocs.outputDir"); + if (StringUtils.hasText(outputDir)) { + return new File(outputDir).getAbsoluteFile(); + } + return null; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index 2745029a3..cd1d7c7c0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -205,14 +205,21 @@ protected PrintStream createPrintStream() if (!outputFile.isAbsolute()) { outputFile = makeAbsolute(outputFile); } - outputFile.getParentFile().mkdirs(); - - return new PrintStream(new FileOutputStream(outputFile)); + + if (outputFile != null) { + outputFile.getParentFile().mkdirs(); + return new PrintStream(new FileOutputStream(outputFile)); + } + + return System.out; } private static File makeAbsolute(File outputFile) { - return new File(new DocumentationProperties().getOutputDir(), - outputFile.getPath()); + File outputDir = new DocumentationProperties().getOutputDir(); + if (outputDir != null) { + return new File(outputDir, outputFile.getPath()); + } + return null; } } diff --git a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index a92f7ab8d..000000000 --- a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,295 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=false -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=false -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=90 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=8 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=90 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.groovy.core.prefs b/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.groovy.core.prefs deleted file mode 100644 index a7f72ec66..000000000 --- a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.groovy.core.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -groovy.compiler.level=23 diff --git a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 0fc7e2d29..000000000 --- a/spring-restdocs-gradle-plugin/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,62 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring Rest Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -formatter_profile=_Spring Rest Docs Java Conventions -formatter_settings_version=12 diff --git a/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationPlugin.groovy b/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationPlugin.groovy deleted file mode 100644 index abfdd8c25..000000000 --- a/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationPlugin.groovy +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014 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.gradle - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.plugins.ide.eclipse.GenerateEclipseClasspath - - -class RestDocumentationPlugin implements Plugin { - - void apply(Project project) { - project.sourceSets { - documentation { - java { srcDir 'src/documentation/java' } - - compileClasspath = project.files(project.sourceSets.main.output, - project.configurations.documentationCompile) - runtimeClasspath = project.files(project.sourceSets.main.output, - project.sourceSets.documentation.output, - project.configurations.documentationRuntime) - } - } - - project.configurations.documentationCompile.extendsFrom project.configurations.compile - - project.configurations.documentationRuntime.extendsFrom( - project.configurations.runtime, project.configurations.documentationCompile) - - project.tasks.withType(GenerateEclipseClasspath) { - it.classpath.sourceSets += project.sourceSets.documentation - it.classpath.plusConfigurations += [ - project.configurations.documentationRuntime - ] - } - - def restDocumentationSnippets = project.tasks.create('restDocumentationSnippets', RestDocumentationSnippets) - - - project.apply plugin: 'org.asciidoctor.gradle.asciidoctor' - - project.asciidoctor { - dependsOn restDocumentationSnippets - group '' - sourceDir = project.file 'src/documentation/asciidoc' - options = [ - attributes: [ - generated: new File("$project.buildDir/generated-documentation").toURI().toURL(), - 'allow-uri-read': true - ] - ] - inputs.files restDocumentationSnippets.outputs.files - } - - project.task('restDocumentation') { - dependsOn ':asciidoctor' - description 'Generates RESTful service documentation using AsciiDoctor' - group 'Documentation' - } - } -} \ No newline at end of file diff --git a/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationSnippets.groovy b/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationSnippets.groovy deleted file mode 100644 index 416d35bd3..000000000 --- a/spring-restdocs-gradle-plugin/src/main/groovy/org/springframework/restdocs/gradle/RestDocumentationSnippets.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014 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.gradle; - -import org.gradle.api.tasks.testing.Test - -public class RestDocumentationSnippets extends Test { - - RestDocumentationSnippets() { - description 'Generates snippets for inclusion in the documentation' - inputs.source project.sourceSets.documentation.java.srcDirs - testClassesDir = project.sourceSets.documentation.output.classesDir - classpath = project.sourceSets.documentation.runtimeClasspath - systemProperty 'org.springframework.restdocs.outputDir', new File(project.buildDir, 'generated-documentation') - } -} diff --git a/spring-restdocs-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.restdocs.properties b/spring-restdocs-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.restdocs.properties deleted file mode 100644 index 30ca3bcf4..000000000 --- a/spring-restdocs-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.restdocs.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=org.springframework.restdocs.gradle.RestDocumentationPlugin \ No newline at end of file From af93a32bde5930c62206637409c8961234796231 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2015 18:06:38 +0000 Subject: [PATCH 0004/1059] Reflect snapshots being available from https://repo.spring.io/snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the README and the samples’ build.gradle and pom.xml files to reflect snapshots now being available from https://repo.spring.io/snapshot. Closes #5 --- README.md | 5 +++-- rest-notes-spring-data-rest/build.gradle | 1 + rest-notes-spring-data-rest/pom.xml | 11 +++++++++++ rest-notes-spring-hateoas/build.gradle | 1 + rest-notes-spring-hateoas/pom.xml | 11 +++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 328378e99..02b43d1d2 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,14 @@ For a broader introduction see the [Documenting RESTful APIs][9] presentation. ## Quickstart -The project requires Java 7 or later. It's built with Gradle: +The project requires Java 7 or later. Snapshots are published to +`https://repo.spring.io/snapshot`. Alternatively, it can be built locally using Gradle: ``` $ ./gradlew build install ``` -Once the main project's built, take a look at one of the two sample projects. Both +The quickest way to get started is to look at one of the two sample projects. Both projects implement a RESTful service for creating tagged notes and illustrate the use of Maven or Gradle. The two projects have different implementations: `rest-notes-spring-hateoas` is implemented using Spring MVC and Spring Hateoas where as diff --git a/rest-notes-spring-data-rest/build.gradle b/rest-notes-spring-data-rest/build.gradle index 386a81d22..49da8bd0d 100644 --- a/rest-notes-spring-data-rest/build.gradle +++ b/rest-notes-spring-data-rest/build.gradle @@ -17,6 +17,7 @@ apply plugin: 'eclipse' repositories { mavenLocal() + maven { url 'https://repo.spring.io/snapshot' } mavenCentral() } diff --git a/rest-notes-spring-data-rest/pom.xml b/rest-notes-spring-data-rest/pom.xml index 10a33975d..ad2f3c2ad 100644 --- a/rest-notes-spring-data-rest/pom.xml +++ b/rest-notes-spring-data-rest/pom.xml @@ -96,4 +96,15 @@ + + + spring-snapshots + Spring snapshots + https://repo.spring.io/snapshot + + true + + + + \ No newline at end of file diff --git a/rest-notes-spring-hateoas/build.gradle b/rest-notes-spring-hateoas/build.gradle index 30b85f68a..0f21290bc 100644 --- a/rest-notes-spring-hateoas/build.gradle +++ b/rest-notes-spring-hateoas/build.gradle @@ -17,6 +17,7 @@ apply plugin: 'eclipse' repositories { mavenLocal() + maven { url 'https://repo.spring.io/snapshot' } mavenCentral() } diff --git a/rest-notes-spring-hateoas/pom.xml b/rest-notes-spring-hateoas/pom.xml index 3ed34f892..881626d5a 100644 --- a/rest-notes-spring-hateoas/pom.xml +++ b/rest-notes-spring-hateoas/pom.xml @@ -112,4 +112,15 @@ + + + spring-snapshots + Spring snapshots + https://repo.spring.io/snapshot + + true + + + + \ No newline at end of file From 8b2174cb841bb3049dddca8a67e3f873b232a7de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jan 2015 18:11:40 +0000 Subject: [PATCH 0005/1059] Provide build status in the README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02b43d1d2..fc6e7feb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## Goal +# Spring REST docs [![Build status][10]][11] -The primary goal of the project is to make it easy to document RESTful services by +The primary goal of this project is to make it easy to document RESTful services by combining content that's been hand-written with auto-generated examples produced with the [Spring MVC Test][2] framework. The result is intended to be an easy-to-read user guide, akin to [GitHub's API documentation][3] for example, rather than the fully @@ -278,3 +278,5 @@ To learn more, take a look at the accompanying sample projects: [7]: rest-notes-spring-data-rest [8]: rest-notes-spring-hateoas [9]: https://speakerdeck.com/ankinson/documenting-restful-apis +[10]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) +[11]: https://build.spring.io/browse/SRD-PUB From bbec60bf8e34dec8b2be0c8dae2677f162bc4146 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2015 12:11:44 +0000 Subject: [PATCH 0006/1059] Apply consistent formatting to the core project --- .../.settings/org.eclipse.jdt.core.prefs | 4 +- .../.settings/org.eclipse.jdt.core.prefs | 4 +- .../.settings/org.eclipse.jdt.core.prefs | 4 +- .../.settings/org.eclipse.jdt.ui.prefs | 59 +++++++++++++++++++ .../core/DocumentationProperties.java | 3 +- .../restdocs/core/DocumentationWriter.java | 4 +- .../restdocs/core/RestDocumentation.java | 31 +++++----- .../core/RestDocumentationResultHandler.java | 45 +++++++------- .../core/RestDocumentationResultHandlers.java | 42 +++++++------ 9 files changed, 134 insertions(+), 62 deletions(-) diff --git a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs b/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs index a92f7ab8d..03628c92c 100644 --- a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs +++ b/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs @@ -36,8 +36,8 @@ org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_field=1 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 diff --git a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs b/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs index a92f7ab8d..03628c92c 100644 --- a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs +++ b/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs @@ -36,8 +36,8 @@ org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_field=1 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs index 521f41600..83412a90a 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs @@ -36,8 +36,8 @@ org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_field=1 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs index 0fc7e2d29..0abc6562c 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs @@ -58,5 +58,64 @@ cleanup.use_type_arguments=false cleanup_profile=_Spring Rest Docs Cleanup Conventions cleanup_settings_version=2 eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_Spring Rest Docs Java Conventions formatter_settings_version=12 +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=false +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_type_arguments=false diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java index 8ecc84b5b..fe644def6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java @@ -51,7 +51,8 @@ class DocumentationProperties { } File getOutputDir() { - String outputDir = this.properties.getProperty("org.springframework.restdocs.outputDir"); + String outputDir = this.properties + .getProperty("org.springframework.restdocs.outputDir"); if (StringUtils.hasText(outputDir)) { return new File(outputDir).getAbsoluteFile(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java index a3bdbc8ca..178bb37b9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java @@ -38,7 +38,8 @@ public void perform() throws Exception { }); } - public void codeBlock(String language, DocumentationAction... actions) throws Exception { + public void codeBlock(String language, DocumentationAction... actions) + throws Exception { println(); if (language != null) { println("[source," + language + "]"); @@ -52,6 +53,7 @@ public void codeBlock(String language, DocumentationAction... actions) throws Ex } public interface DocumentationAction { + void perform() throws Exception; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java index 2aa9865ac..160119caa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java @@ -20,22 +20,23 @@ public class RestDocumentation { - public static RestDocumentationResultHandler document(String outputDir) throws Exception { - return new RestDocumentationResultHandler(outputDir); - } + public static RestDocumentationResultHandler document(String outputDir) + throws Exception { + return new RestDocumentationResultHandler(outputDir); + } - public static LinkDescriptor linkWithRel(String rel) { - return new LinkDescriptor(rel); - } + public static LinkDescriptor linkWithRel(String rel) { + return new LinkDescriptor(rel); + } - public static LinkExtractor halLinks() { - return new LinkExtractor() { + public static LinkExtractor halLinks() { + return new LinkExtractor() { - @SuppressWarnings("unchecked") - @Override - public Map extractLinks(Map responseJson) { - return (Map) responseJson.get("_links"); - } - }; - } + @SuppressWarnings("unchecked") + @Override + public Map extractLinks(Map responseJson) { + return (Map) responseJson.get("_links"); + } + }; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java index 1e00cf4d9..e1b96b849 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java @@ -28,25 +28,28 @@ public class RestDocumentationResultHandler implements ResultHandler { - private final String outputDir; - private ResultHandler linkDocumentingResultHandler; - - public RestDocumentationResultHandler(String outputDir) { - this.outputDir = outputDir; - } - - @Override - public void handle(MvcResult result) throws Exception { - documentCurlRequest(outputDir).includeResponseHeaders().handle(result); - documentCurlResponse(outputDir).includeResponseHeaders().handle(result); - documentCurlRequestAndResponse(outputDir).includeResponseHeaders().handle(result); - if(linkDocumentingResultHandler != null) { - linkDocumentingResultHandler.handle(result); - } - } - - public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - linkDocumentingResultHandler = new LinkDocumentingResultHandler(outputDir, linkExtractor, Arrays.asList(descriptors)); - return this; - } + private final String outputDir; + + private ResultHandler linkDocumentingResultHandler; + + public RestDocumentationResultHandler(String outputDir) { + this.outputDir = outputDir; + } + + @Override + public void handle(MvcResult result) throws Exception { + documentCurlRequest(outputDir).includeResponseHeaders().handle(result); + documentCurlResponse(outputDir).includeResponseHeaders().handle(result); + documentCurlRequestAndResponse(outputDir).includeResponseHeaders().handle(result); + if (linkDocumentingResultHandler != null) { + linkDocumentingResultHandler.handle(result); + } + } + + public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + linkDocumentingResultHandler = new LinkDocumentingResultHandler(outputDir, + linkExtractor, Arrays.asList(descriptors)); + return this; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index cd1d7c7c0..04afb317e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -16,8 +16,8 @@ package org.springframework.restdocs.core; -import static org.springframework.restdocs.core.IterableEnumeration.iterable; import static org.junit.Assert.fail; +import static org.springframework.restdocs.core.IterableEnumeration.iterable; import java.io.File; import java.io.FileNotFoundException; @@ -47,6 +47,7 @@ public abstract class RestDocumentationResultHandlers { public static CurlResultHandler documentCurlRequest(String outputDir) { return new CurlResultHandler(outputDir, "request") { + @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { @@ -58,24 +59,26 @@ public void handle(MvcResult result, DocumentationWriter writer) public static CurlResultHandler documentCurlResponse(String outputDir) { return new CurlResultHandler(outputDir, "response") { + @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, result, - getCurlConfiguration())); + writer.codeBlock("http", new CurlResponseDocumentationAction(writer, + result, getCurlConfiguration())); } }; } public static CurlResultHandler documentCurlRequestAndResponse(String outputDir) { return new CurlResultHandler(outputDir, "request-response") { + @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { writer.shellCommand(new CurlRequestDocumentationAction(writer, result, getCurlConfiguration())); - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, result, - getCurlConfiguration())); + writer.codeBlock("http", new CurlResponseDocumentationAction(writer, + result, getCurlConfiguration())); } }; } @@ -174,16 +177,16 @@ private static class CurlConfiguration { } public static abstract class RestDocumentationResultHandler implements ResultHandler { - + private String outputDir; - + private String fileName; public RestDocumentationResultHandler(String outputDir, String fileName) { this.outputDir = outputDir; this.fileName = fileName; } - + abstract void handle(MvcResult result, DocumentationWriter writer) throws Exception; @@ -198,19 +201,18 @@ public void handle(MvcResult result) throws Exception { } } - protected PrintStream createPrintStream() - throws FileNotFoundException { - + protected PrintStream createPrintStream() throws FileNotFoundException { + File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); if (!outputFile.isAbsolute()) { outputFile = makeAbsolute(outputFile); } - + if (outputFile != null) { outputFile.getParentFile().mkdirs(); return new PrintStream(new FileOutputStream(outputFile)); } - + return System.out; } @@ -249,10 +251,11 @@ static class LinkDocumentingResultHandler extends RestDocumentationResultHandler private final LinkExtractor extractor; - public LinkDocumentingResultHandler(String outputDir, LinkExtractor linkExtractor, List descriptors) { + public LinkDocumentingResultHandler(String outputDir, + LinkExtractor linkExtractor, List descriptors) { super(outputDir, "links"); this.extractor = linkExtractor; - for (LinkDescriptor descriptor: descriptors) { + for (LinkDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getRel()); Assert.hasText(descriptor.getDescription()); this.descriptorsByRel.put(descriptor.getRel(), descriptor); @@ -262,7 +265,8 @@ public LinkDocumentingResultHandler(String outputDir, LinkExtractor linkExtracto @SuppressWarnings("unchecked") @Override void handle(MvcResult result, DocumentationWriter writer) throws Exception { - Map json = this.objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + Map json = this.objectMapper.readValue(result.getResponse() + .getContentAsString(), Map.class); Map links = this.extractor.extractLinks(json); Set actualRels = links.keySet(); @@ -277,10 +281,12 @@ void handle(MvcResult result, DocumentationWriter writer) throws Exception { if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { String message = ""; if (!undocumentedRels.isEmpty()) { - message += "Links with the following relations were not documented: " + undocumentedRels; + message += "Links with the following relations were not documented: " + + undocumentedRels; } if (!missingRels.isEmpty()) { - message += "Links with the following relations were not found in the response: " + missingRels; + message += "Links with the following relations were not found in the response: " + + missingRels; } fail(message); } From 78736d6c506f284ee256453fd2031fb0c785d61e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Jan 2015 16:25:13 +0000 Subject: [PATCH 0007/1059] Improve the abstraction that's used for link extraction Previously, the link extraction abstraction was inadequate as it made assumptions about the format of the links that did not apply to all media types. For example, it was suited to the links returned in a application/hal+json response, but was not suited to the Atom-style links often found in application/json responses. This commit improves the abstraction so that the extracted links are decoupled from the format of the response. In addition to the existing support for extracting HAL links a new extractor for Atom-style links has been introduced. Both extractors are now available via static methods on the new LinkExtractors class. The sample applications have been updated accordingly. Closes #6 --- .../notes/RestNotesSpringDataRest.java | 2 +- .../com/example/notes/ApiDocumentation.java | 4 +- .../com/example/notes/ApiDocumentation.java | 4 +- .../springframework/restdocs/core/Link.java | 94 ++++++++++ .../restdocs/core/LinkExtractor.java | 24 ++- .../restdocs/core/LinkExtractors.java | 162 ++++++++++++++++++ .../restdocs/core/RestDocumentation.java | 14 +- .../core/RestDocumentationResultHandlers.java | 12 +- .../restdocs/core/LinkExtractorsTests.java | 124 ++++++++++++++ .../atom/multiple-links-different-rels.json | 9 + .../atom/multiple-links-same-rels.json | 9 + .../link-payloads/atom/no-links.json | 1 + .../link-payloads/atom/single-link.json | 6 + .../link-payloads/atom/wrong-format.json | 5 + .../hal/multiple-links-different-rels.json | 6 + .../hal/multiple-links-same-rels.json | 5 + .../resources/link-payloads/hal/no-links.json | 1 + .../link-payloads/hal/single-link.json | 5 + .../link-payloads/hal/wrong-format.json | 9 + 19 files changed, 467 insertions(+), 29 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json create mode 100644 spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java index 0c995e301..74b98dd0c 100644 --- a/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ b/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. diff --git a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index e3d264189..f3f95d8e8 100644 --- a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.halLinks; +import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; diff --git a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 745c4bfe3..da8ca7247 100644 --- a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -18,8 +18,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.halLinks; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java new file mode 100644 index 000000000..135a51e44 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java @@ -0,0 +1,94 @@ +/* + * 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.core; + +import org.springframework.core.style.ToStringCreator; + +/** + * Representation of a link used in a Hypermedia-based API + * + * @author Andy Wilkinson + */ +public class Link { + + private final String rel; + + private final String href; + + /** + * Creates a new {@code Link} with the given {@code rel} and {@code href} + * + * @param rel The link's rel + * @param href The link's href + */ + public Link(String rel, String href) { + this.rel = rel; + this.href = href; + } + + /** + * Returns the link's {@code rel} + * @return the link's {@code rel} + */ + public String getRel() { + return rel; + } + + /** + * Returns the link's {@code href} + * @return the link's {@code href} + */ + public String getHref() { + return href; + } + + @Override + public int hashCode() { + int prime = 31; + int result = 1; + result = prime * result + href.hashCode(); + result = prime * result + rel.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; + } + Link other = (Link) obj; + if (!href.equals(other.href)) { + return false; + } + if (!rel.equals(other.rel)) { + return false; + } + return true; + } + + public String toString() { + return new ToStringCreator(this).append("rel", this.rel) + .append("href", this.href).toString(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java index 3df9e7b89..bb65fd8e8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -16,10 +16,30 @@ package org.springframework.restdocs.core; +import java.io.IOException; +import java.util.List; import java.util.Map; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The + * expected format of the links in the response is determined by the implementation. + * + * @author Andy Wilkinson + * + */ public interface LinkExtractor { - Map extractLinks(Map responseJson); + /** + * Extract the links from the given response, returning a {@code Map} of links where + * the keys are the link rels. + * + * @param response The response from which the links are to be extracted + * @return The extracted links, keyed by rel + * @throws IOException if link extraction fails + */ + Map> extractLinks(MockHttpServletResponse response) + throws IOException; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java new file mode 100644 index 000000000..e6c28d530 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java @@ -0,0 +1,162 @@ +/* + * 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.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.mock.web.MockHttpServletResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Static factory methods provided a selection of {@link LinkExtractor link extractors} + * for use when documentating a hypermedia-based API. + * + * @author Andy Wilkinson + * + */ +public class LinkExtractors { + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia + * Application Language (HAL) format where the links are found in a map named + * {@code _links}. + * + * @return The extract for HAL-style links + */ + public static LinkExtractor halLinks() { + return new HalLinkExtractor(); + } + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Atom format where + * the links are found in an array named {@code links}. + * + * @return The extractor for Atom-style links + */ + public static LinkExtractor atomLinks() { + return new AtomLinkExtractor(); + } + + private static abstract class JsonContentLinkExtractor implements LinkExtractor { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressWarnings("unchecked") + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + Map jsonContent = this.objectMapper.readValue( + response.getContentAsString(), Map.class); + return extractLinks(jsonContent); + } + + protected abstract Map> extractLinks(Map json); + } + + private static class HalLinkExtractor extends JsonContentLinkExtractor { + + @SuppressWarnings("unchecked") + @Override + public Map> extractLinks(Map json) { + Map> extractedLinks = new HashMap<>(); + Object possibleLinks = json.get("_links"); + if (possibleLinks instanceof Map) { + Map links = (Map) possibleLinks; + for (Entry entry : links.entrySet()) { + String rel = (String) entry.getKey(); + extractedLinks.put(rel, convertToLinks(entry.getValue(), rel)); + } + } + return extractedLinks; + } + + @SuppressWarnings("unchecked") + private static List convertToLinks(Object object, String rel) { + List links = new ArrayList<>(); + if (object instanceof Collection) { + Collection hrefObjects = (Collection) object; + for (Object hrefObject : hrefObjects) { + maybeAddLink(maybeCreateLink(rel, hrefObject), links); + } + } + else { + maybeAddLink(maybeCreateLink(rel, object), links); + } + return links; + } + + private static Link maybeCreateLink(String rel, Object possibleHref) { + if (possibleHref instanceof String) { + return new Link(rel, (String) possibleHref); + } + return null; + } + + private static void maybeAddLink(Link possibleLink, List links) { + if (possibleLink != null) { + links.add(possibleLink); + } + } + } + + private static class AtomLinkExtractor extends JsonContentLinkExtractor { + + @SuppressWarnings("unchecked") + @Override + public Map> extractLinks(Map json) { + Map> extractedLinks = new HashMap<>(); + Object possibleLinks = json.get("links"); + if (possibleLinks instanceof Collection) { + Collection linksCollection = (Collection) possibleLinks; + for (Object linkObject : linksCollection) { + if (linkObject instanceof Map) { + Link link = maybeCreateLink((Map) linkObject); + maybeStoreLink(link, extractedLinks); + } + } + } + return extractedLinks; + } + + private static Link maybeCreateLink(Map linkMap) { + Object hrefObject = linkMap.get("href"); + Object relObject = linkMap.get("rel"); + if (relObject instanceof String && hrefObject instanceof String) { + return new Link((String) relObject, (String) hrefObject); + } + return null; + } + + private static void maybeStoreLink(Link link, + Map> extractedLinks) { + if (link != null) { + List linksForRel = extractedLinks.get(link.getRel()); + if (linksForRel == null) { + linksForRel = new ArrayList(); + extractedLinks.put(link.getRel(), linksForRel); + } + linksForRel.add(link); + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java index 160119caa..974e1dbb6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -16,8 +16,6 @@ package org.springframework.restdocs.core; -import java.util.Map; - public class RestDocumentation { public static RestDocumentationResultHandler document(String outputDir) @@ -29,14 +27,4 @@ public static LinkDescriptor linkWithRel(String rel) { return new LinkDescriptor(rel); } - public static LinkExtractor halLinks() { - return new LinkExtractor() { - - @SuppressWarnings("unchecked") - @Override - public Map extractLinks(Map responseJson) { - return (Map) responseJson.get("_links"); - } - }; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index 04afb317e..256279d0e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -41,8 +41,6 @@ import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.RequestMethod; -import com.fasterxml.jackson.databind.ObjectMapper; - public abstract class RestDocumentationResultHandlers { public static CurlResultHandler documentCurlRequest(String outputDir) { @@ -245,8 +243,6 @@ public CurlResultHandler includeResponseHeaders() { static class LinkDocumentingResultHandler extends RestDocumentationResultHandler { - private final ObjectMapper objectMapper = new ObjectMapper(); - private final Map descriptorsByRel = new HashMap(); private final LinkExtractor extractor; @@ -262,12 +258,10 @@ public LinkDocumentingResultHandler(String outputDir, } } - @SuppressWarnings("unchecked") @Override void handle(MvcResult result, DocumentationWriter writer) throws Exception { - Map json = this.objectMapper.readValue(result.getResponse() - .getContentAsString(), Map.class); - Map links = this.extractor.extractLinks(json); + Map> links = this.extractor.extractLinks(result + .getResponse()); Set actualRels = links.keySet(); Set expectedRels = this.descriptorsByRel.keySet(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java new file mode 100644 index 000000000..2e94feadb --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java @@ -0,0 +1,124 @@ +/* + * 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.core; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Tests for {@link LinkExtractors}. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class LinkExtractorsTests { + + private final LinkExtractor linkExtractor; + + private final String linkType; + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, + new Object[] { LinkExtractors.atomLinks(), "atom" }); + } + + public LinkExtractorsTests(LinkExtractor linkExtractor, String linkType) { + this.linkExtractor = linkExtractor; + this.linkType = linkType; + } + + @Test + public void singleLink() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("single-link")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + } + + @Test + public void multipleLinksWithDifferentRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-different-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), + new Link("bravo", "http://bravo.example.com")), links); + } + + @Test + public void multipleLinksWithSameRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-same-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), + new Link("alpha", "http://alpha.example.com/two")), links); + } + + @Test + public void noLinks() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("no-links")); + assertLinks(Collections. emptyList(), links); + } + + @Test + public void linksInTheWrongFormat() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("wrong-format")); + assertLinks(Collections. emptyList(), links); + } + + private void assertLinks(List expectedLinks, Map> actualLinks) { + Map> expectedLinksByRel = new HashMap<>(); + for (Link expectedLink : expectedLinks) { + List expectedlinksWithRel = expectedLinksByRel.get(expectedLink + .getRel()); + if (expectedlinksWithRel == null) { + expectedlinksWithRel = new ArrayList<>(); + expectedLinksByRel.put(expectedLink.getRel(), expectedlinksWithRel); + } + expectedlinksWithRel.add(expectedLink); + } + assertEquals(expectedLinksByRel, actualLinks); + } + + 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/link-payloads/" + linkType + "/" + name + + ".json"); + } +} diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json new file mode 100644 index 000000000..35e63537d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json @@ -0,0 +1,9 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com" + }, { + "rel": "bravo", + "href": "http://bravo.example.com" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json new file mode 100644 index 000000000..ff0d3f4ae --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json @@ -0,0 +1,9 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com/one" + }, { + "rel": "alpha", + "href": "http://alpha.example.com/two" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json new file mode 100644 index 000000000..6f31cf5a2 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json new file mode 100644 index 000000000..57532675b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json @@ -0,0 +1,6 @@ +{ + "links": [ { + "rel": "alpha", + "href": "http://alpha.example.com" + } ] +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json new file mode 100644 index 000000000..04a6d84b8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json new file mode 100644 index 000000000..80d36d72a --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -0,0 +1,6 @@ +{ + "_links": { + "alpha": "http://alpha.example.com", + "bravo": "http://bravo.example.com" + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json new file mode 100644 index 000000000..04a6d84b8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json new file mode 100644 index 000000000..6f31cf5a2 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json new file mode 100644 index 000000000..be90b3779 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -0,0 +1,5 @@ +{ + "_links": { + "alpha": "http://alpha.example.com" + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json new file mode 100644 index 000000000..7c99aa4ef --- /dev/null +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json @@ -0,0 +1,9 @@ +{ + "_links": [ { + "rel": "alpha", + "href": "http://alpha.example.com/one" + }, { + "rel": "alpha", + "href": "http://alpha.example.com/two" + } ] +} \ No newline at end of file From b57eb002dc76f504a9723ffc11d16c379c93a6cd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2015 16:25:13 +0000 Subject: [PATCH 0008/1059] Consistent use of this when accessing fields in the core project --- .../.settings/org.eclipse.jdt.ui.prefs | 2 +- .../.settings/org.eclipse.jdt.ui.prefs | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 10 +++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 22 +++++++++++-------- .../springframework/restdocs/core/Link.java | 17 +++++++------- .../restdocs/core/LinkDescriptor.java | 4 ++-- .../restdocs/core/LinkExtractor.java | 4 ++-- .../restdocs/core/LinkExtractors.java | 9 ++++---- .../core/RestDocumentationConfiguration.java | 8 +++---- .../core/RestDocumentationResultHandler.java | 16 ++++++++------ .../restdocs/core/LinkExtractorsTests.java | 4 ++-- 11 files changed, 58 insertions(+), 40 deletions(-) diff --git a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs b/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs index 0fc7e2d29..6518db6af 100644 --- a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs +++ b/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs @@ -50,7 +50,7 @@ cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access=true cleanup.use_this_for_non_static_field_access_only_if_necessary=false cleanup.use_this_for_non_static_method_access=false cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs b/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs index 0fc7e2d29..6518db6af 100644 --- a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs +++ b/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs @@ -50,7 +50,7 @@ cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access=true cleanup.use_this_for_non_static_field_access_only_if_necessary=false cleanup.use_this_for_non_static_method_access=false cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs index 83412a90a..f96273611 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,14 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.codeComplete.argumentPrefixes= +org.eclipse.jdt.core.codeComplete.argumentSuffixes= +org.eclipse.jdt.core.codeComplete.fieldPrefixes= +org.eclipse.jdt.core.codeComplete.fieldSuffixes= +org.eclipse.jdt.core.codeComplete.localPrefixes= +org.eclipse.jdt.core.codeComplete.localSuffixes= +org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs index 0abc6562c..ca171a907 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs @@ -50,7 +50,7 @@ cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access=true cleanup.use_this_for_non_static_field_access_only_if_necessary=false cleanup.use_this_for_non_static_method_access=false cleanup.use_this_for_non_static_method_access_only_if_necessary=true @@ -61,6 +61,10 @@ eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_Spring Rest Docs Java Conventions formatter_settings_version=12 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=true +org.eclipse.jdt.ui.keywordthis=true +org.eclipse.jdt.ui.overrideannotation=true sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true @@ -72,8 +76,8 @@ sp_cleanup.add_missing_override_annotations_interface_methods=true sp_cleanup.add_serial_version_id=false sp_cleanup.always_use_blocks=true sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.always_use_this_for_non_static_field_access=true +sp_cleanup.always_use_this_for_non_static_method_access=true sp_cleanup.convert_functional_interfaces=false sp_cleanup.convert_to_enhanced_for_loop=false sp_cleanup.correct_indentation=false @@ -87,12 +91,12 @@ sp_cleanup.make_type_abstract_if_missing_method=false sp_cleanup.make_variable_declarations_final=false sp_cleanup.never_use_blocks=false sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=false +sp_cleanup.on_save_use_additional_actions=true sp_cleanup.organize_imports=true sp_cleanup.qualify_static_field_accesses_with_declaring_class=false sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_with_declaring_class=true sp_cleanup.qualify_static_method_accesses_with_declaring_class=false sp_cleanup.remove_private_constructors=true sp_cleanup.remove_redundant_type_arguments=true @@ -114,8 +118,8 @@ sp_cleanup.use_blocks=false sp_cleanup.use_blocks_only_for_return_and_throw=false sp_cleanup.use_lambda=true sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_field_access=true +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false +sp_cleanup.use_this_for_non_static_method_access=true +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=false sp_cleanup.use_type_arguments=false diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java index 135a51e44..ef1534878 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java @@ -20,7 +20,7 @@ /** * Representation of a link used in a Hypermedia-based API - * + * * @author Andy Wilkinson */ public class Link { @@ -31,7 +31,7 @@ public class Link { /** * Creates a new {@code Link} with the given {@code rel} and {@code href} - * + * * @param rel The link's rel * @param href The link's href */ @@ -45,7 +45,7 @@ public Link(String rel, String href) { * @return the link's {@code rel} */ public String getRel() { - return rel; + return this.rel; } /** @@ -53,15 +53,15 @@ public String getRel() { * @return the link's {@code href} */ public String getHref() { - return href; + return this.href; } @Override public int hashCode() { int prime = 31; int result = 1; - result = prime * result + href.hashCode(); - result = prime * result + rel.hashCode(); + result = prime * result + this.href.hashCode(); + result = prime * result + this.rel.hashCode(); return result; } @@ -77,15 +77,16 @@ public boolean equals(Object obj) { return false; } Link other = (Link) obj; - if (!href.equals(other.href)) { + if (!this.href.equals(other.href)) { return false; } - if (!rel.equals(other.rel)) { + if (!this.rel.equals(other.rel)) { return false; } return true; } + @Override public String toString() { return new ToStringCreator(this).append("rel", this.rel) .append("href", this.href).toString(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java index b6c117f0e..e28a8af4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java @@ -32,10 +32,10 @@ public LinkDescriptor description(String description) { } String getRel() { - return rel; + return this.rel; } String getDescription() { - return description; + return this.description; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java index bb65fd8e8..f63d2c4f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java @@ -25,7 +25,7 @@ /** * A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The * expected format of the links in the response is determined by the implementation. - * + * * @author Andy Wilkinson * */ @@ -34,7 +34,7 @@ public interface LinkExtractor { /** * Extract the links from the given response, returning a {@code Map} of links where * the keys are the link rels. - * + * * @param response The response from which the links are to be extracted * @return The extracted links, keyed by rel * @throws IOException if link extraction fails diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java index e6c28d530..f736e07db 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java @@ -31,7 +31,7 @@ /** * Static factory methods provided a selection of {@link LinkExtractor link extractors} * for use when documentating a hypermedia-based API. - * + * * @author Andy Wilkinson * */ @@ -41,7 +41,7 @@ public class LinkExtractors { * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named * {@code _links}. - * + * * @return The extract for HAL-style links */ public static LinkExtractor halLinks() { @@ -51,7 +51,7 @@ public static LinkExtractor halLinks() { /** * Returns a {@code LinkExtractor} capable of extracting links in Atom format where * the links are found in an array named {@code links}. - * + * * @return The extractor for Atom-style links */ public static LinkExtractor atomLinks() { @@ -62,6 +62,7 @@ private static abstract class JsonContentLinkExtractor implements LinkExtractor private final ObjectMapper objectMapper = new ObjectMapper(); + @Override @SuppressWarnings("unchecked") public Map> extractLinks(MockHttpServletResponse response) throws IOException { @@ -83,7 +84,7 @@ public Map> extractLinks(Map json) { if (possibleLinks instanceof Map) { Map links = (Map) possibleLinks; for (Entry entry : links.entrySet()) { - String rel = (String) entry.getKey(); + String rel = entry.getKey(); extractedLinks.put(rel, convertToLinks(entry.getValue(), rel)); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java index 1fdd1d44e..70a7c70ed 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java @@ -53,10 +53,10 @@ public RequestPostProcessor beforeMockMvcCreated( @Override public MockHttpServletRequest postProcessRequest( MockHttpServletRequest request) { - request.setScheme(scheme); - request.setRemotePort(port); - request.setServerPort(port); - request.setRemoteHost(host); + request.setScheme(RestDocumentationConfiguration.this.scheme); + request.setRemotePort(RestDocumentationConfiguration.this.port); + request.setServerPort(RestDocumentationConfiguration.this.port); + request.setRemoteHost(RestDocumentationConfiguration.this.host); return request; } }; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java index e1b96b849..71eca2941 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java @@ -38,18 +38,20 @@ public RestDocumentationResultHandler(String outputDir) { @Override public void handle(MvcResult result) throws Exception { - documentCurlRequest(outputDir).includeResponseHeaders().handle(result); - documentCurlResponse(outputDir).includeResponseHeaders().handle(result); - documentCurlRequestAndResponse(outputDir).includeResponseHeaders().handle(result); - if (linkDocumentingResultHandler != null) { - linkDocumentingResultHandler.handle(result); + documentCurlRequest(this.outputDir).includeResponseHeaders().handle(result); + documentCurlResponse(this.outputDir).includeResponseHeaders().handle(result); + documentCurlRequestAndResponse(this.outputDir).includeResponseHeaders().handle( + result); + if (this.linkDocumentingResultHandler != null) { + this.linkDocumentingResultHandler.handle(result); } } public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - linkDocumentingResultHandler = new LinkDocumentingResultHandler(outputDir, - linkExtractor, Arrays.asList(descriptors)); + this.linkDocumentingResultHandler = new LinkDocumentingResultHandler( + this.outputDir, linkExtractor, Arrays.asList(descriptors)); return this; } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java index 2e94feadb..1794b7d19 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java @@ -38,7 +38,7 @@ /** * Tests for {@link LinkExtractors}. - * + * * @author Andy Wilkinson */ @RunWith(Parameterized.class) @@ -118,7 +118,7 @@ private MockHttpServletResponse createResponse(String contentName) throws IOExce } private File getPayloadFile(String name) { - return new File("src/test/resources/link-payloads/" + linkType + "/" + name + return new File("src/test/resources/link-payloads/" + this.linkType + "/" + name + ".json"); } } From cae7e3fb582d024f924943828b6721494610730f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Jan 2015 16:41:38 +0000 Subject: [PATCH 0009/1059] Add support for determining the link extractor from the content type Previously, when documenting links, it was necessary to provide the link extractor that should be used to get the links from the response. This commit improves on this by adding support for determining the link extractor to use based on the response's content type. Two content types are currently supported; application/hal+json and application/json. For other content types, an extractor can be provided as before when calling withLinks. Closes #7 --- .../com/example/notes/ApiDocumentation.java | 7 +++---- .../com/example/notes/ApiDocumentation.java | 7 +++---- .../.settings/org.eclipse.jdt.ui.prefs | 2 +- .../restdocs/core/LinkExtractors.java | 21 +++++++++++++++++-- .../core/RestDocumentationResultHandler.java | 4 ++++ .../core/RestDocumentationResultHandlers.java | 20 ++++++++++++++++-- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index f3f95d8e8..c7ae09466 100644 --- a/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -19,7 +19,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; @@ -95,7 +94,7 @@ public void errorExample() throws Exception { public void indexExample() throws Exception { this.mockMvc.perform(get("/")) .andExpect(status().isOk()) - .andDo(document("index-example").withLinks(halLinks(), + .andDo(document("index-example").withLinks( linkWithRel("notes").description( "The <>"), linkWithRel("tags").description( @@ -175,7 +174,7 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.tags", is(notNullValue()))) .andDo(document("note-get-example") - .withLinks(halLinks(), + .withLinks( linkWithRel("self").description("This <>"), linkWithRel("tags").description( "This note's <>"))); @@ -262,7 +261,7 @@ public void tagGetExample() throws Exception { this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example").withLinks(halLinks(), + .andDo(document("tag-get-example").withLinks( linkWithRel("self").description("This <>"), linkWithRel("notes") .description( diff --git a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index da8ca7247..7e201ed2b 100644 --- a/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -18,7 +18,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.core.LinkExtractors.halLinks; import static org.springframework.restdocs.core.RestDocumentation.document; import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -95,7 +94,7 @@ public void errorExample() throws Exception { public void indexExample() throws Exception { this.mockMvc.perform(get("/")) .andExpect(status().isOk()) - .andDo(document("index-example").withLinks(halLinks(), + .andDo(document("index-example").withLinks( linkWithRel("notes").description( "The <>"), linkWithRel("tags").description( @@ -171,7 +170,7 @@ public void noteGetExample() throws Exception { .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(halLinks(), + .andDo(document("note-get-example").withLinks( linkWithRel("self").description("This <>"), linkWithRel("note-tags").description( "This note's <>"))); @@ -258,7 +257,7 @@ public void tagGetExample() throws Exception { this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example").withLinks(halLinks(), + .andDo(document("tag-get-example").withLinks( linkWithRel("self").description("This <>"), linkWithRel("tagged-notes") .description( diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs index ca171a907..dc84ae74c 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs @@ -120,6 +120,6 @@ sp_cleanup.use_lambda=true sp_cleanup.use_parentheses_in_expressions=false sp_cleanup.use_this_for_non_static_field_access=true sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=true +sp_cleanup.use_this_for_non_static_method_access=false sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=false sp_cleanup.use_type_arguments=false diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java index f736e07db..df35102e6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java @@ -24,16 +24,16 @@ import java.util.Map; import java.util.Map.Entry; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; /** - * Static factory methods provided a selection of {@link LinkExtractor link extractors} + * Static factory methods providing a selection of {@link LinkExtractor link extractors} * for use when documentating a hypermedia-based API. * * @author Andy Wilkinson - * */ public class LinkExtractors { @@ -58,6 +58,23 @@ public static LinkExtractor atomLinks() { return new AtomLinkExtractor(); } + /** + * Returns the {@code LinkExtractor} for the given {@code contentType} or {@code null} + * if there is no extractor for the content type. + * + * @param contentType The content type + * @return The extractor for the content type, or {@code null} + */ + public static LinkExtractor extractorForContentType(String contentType) { + if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + return atomLinks(); + } + else if ("application/hal+json".equals(contentType)) { + return halLinks(); + } + return null; + } + private static abstract class JsonContentLinkExtractor implements LinkExtractor { private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java index 71eca2941..a5464bfd5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java @@ -47,6 +47,10 @@ public void handle(MvcResult result) throws Exception { } } + public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { + return withLinks(null, descriptors); + } + public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { this.linkDocumentingResultHandler = new LinkDocumentingResultHandler( diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index 256279d0e..7b066cc28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -260,8 +260,24 @@ public LinkDocumentingResultHandler(String outputDir, @Override void handle(MvcResult result, DocumentationWriter writer) throws Exception { - Map> links = this.extractor.extractLinks(result - .getResponse()); + Map> links; + if (this.extractor != null) { + links = this.extractor.extractLinks(result.getResponse()); + } + else { + String contentType = result.getResponse().getContentType(); + LinkExtractor extractorForContentType = LinkExtractors + .extractorForContentType(contentType); + if (extractorForContentType != null) { + links = extractorForContentType.extractLinks(result.getResponse()); + } + else { + throw new IllegalStateException( + "No LinkExtractor has been provided and one is not available for the content type " + + contentType); + } + + } Set actualRels = links.keySet(); Set expectedRels = this.descriptorsByRel.keySet(); From 5f49aa273240dfe2ca676a5319526a8a7143ab2a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Feb 2015 17:13:53 +0000 Subject: [PATCH 0010/1059] Fix formatting of a code block in the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fc6e7feb3..328f28a0e 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,12 @@ ext { Configure the `test` task with a system property to control the location to which the snippets are generated: +```groovy test { systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation outputs.dir generatedDocumentation } +``` Configure the `asciidoctor` task. The `generated` attribute is used to provide easy access to the generated snippets: From 530a218ab4259976f7afe460b509d6e0f1d018d5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Feb 2015 17:16:32 +0000 Subject: [PATCH 0011/1059] Correct the capitalisation of Asciidoctor in the README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 328f28a0e..fc0c797b0 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Both Maven and Gradle are supported. You can look at either samples' `build.gradle` file to see the required configuration. The key parts are described below. -Configure the AsciiDoctor plugin: +Configure the Asciidoctor plugin: ```groovy plugins { @@ -151,7 +151,7 @@ the snippets are generated: ``` -Configure the AsciiDoctor plugin. The `generated` attribute is used to provide easy +Configure the Asciidoctor plugin. The `generated` attribute is used to provide easy access to the generated snippets: ```xml @@ -232,8 +232,8 @@ Producing high-quality, easily readable documentation is difficult and the proce only made harder by trying to write the documentation in an ill-suited format such as Java annotations. This project addresses this by allowing you to write the bulk of your documentation's text using [Asciidoctor][1]. The default location for source files -depends on whether you're using Maven or Gradle. By default, AsciiDoctor's Maven plugin -looks in `src/main/asciidoc`, whereas the AsciiDoctor Gradle plugin looks in +depends on whether you're using Maven or Gradle. By default, Asciidoctor's Maven plugin +looks in `src/main/asciidoc`, whereas the Asciidoctor Gradle plugin looks in `src/docs/asciidoc` To include the programmatically generated snippets in your documentation, you use From f405848179f359d38be53060053e9b1534d3543b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 11:05:29 +0000 Subject: [PATCH 0012/1059] Document Gradle and Maven configuration for self-hosted documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the README to describe how to include the generated documentation in a project’s jar file so that it can be served as static content by, for example, Spring Boot. Closes gh-12 --- README.md | 57 ++++++++++++++++++++++++ rest-notes-spring-data-rest/build.gradle | 7 +++ rest-notes-spring-data-rest/pom.xml | 23 +++++++++- rest-notes-spring-hateoas/build.gradle | 7 +++ rest-notes-spring-hateoas/pom.xml | 23 +++++++++- 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc0c797b0..ac61f8a64 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,21 @@ asciidoctor { ``` +You may want to include the generated documentation in your project's jar file, for +example to have it [served as static content by Spring Boot][12]. You can do so by +configuring the `jar` task to depend on the `asciidoctor` task and to copy the +generated documentation into the jar's `static` directory: + +```groovy +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + +``` + #### Maven configuration You can look at either samples' `pom.xml` file to see the required configuration. The key @@ -176,7 +191,48 @@ access to the generated snippets: +``` + +You may want to include the generated documentation in your project's jar file, for +example to have it [served as static content by Spring Boot][12]. You can do so by +changing the configuration of the Asciidoctor plugin so that it runs in the +`prepare-package` phase and then configuring Maven's resources plugin to copy the +generated documentation into a location where it'll be included in the project's jar: +```xml + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + prepare-package + + + + + + maven-resources-plugin + 2.7 + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/static/docs + + + ${project.build.directory}/generated-docs + + + + + + ``` ### Programatically generated snippets @@ -282,3 +338,4 @@ To learn more, take a look at the accompanying sample projects: [9]: https://speakerdeck.com/ankinson/documenting-restful-apis [10]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) [11]: https://build.spring.io/browse/SRD-PUB +[12]: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-static-content diff --git a/rest-notes-spring-data-rest/build.gradle b/rest-notes-spring-data-rest/build.gradle index 49da8bd0d..fa6262c94 100644 --- a/rest-notes-spring-data-rest/build.gradle +++ b/rest-notes-spring-data-rest/build.gradle @@ -51,6 +51,13 @@ asciidoctor { dependsOn test } +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + task wrapper(type: Wrapper) { gradleVersion = '2.1' } diff --git a/rest-notes-spring-data-rest/pom.xml b/rest-notes-spring-data-rest/pom.xml index ad2f3c2ad..37b10e6b8 100644 --- a/rest-notes-spring-data-rest/pom.xml +++ b/rest-notes-spring-data-rest/pom.xml @@ -79,7 +79,7 @@ generate-docs - package + prepare-package process-asciidoc @@ -93,6 +93,27 @@ + + maven-resources-plugin + 2.7 + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/static/docs + + + ${project.build.directory}/generated-docs + + + + + + diff --git a/rest-notes-spring-hateoas/build.gradle b/rest-notes-spring-hateoas/build.gradle index 0f21290bc..fff6d0e1f 100644 --- a/rest-notes-spring-hateoas/build.gradle +++ b/rest-notes-spring-hateoas/build.gradle @@ -53,6 +53,13 @@ asciidoctor { dependsOn test } +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + task wrapper(type: Wrapper) { gradleVersion = '2.1' } diff --git a/rest-notes-spring-hateoas/pom.xml b/rest-notes-spring-hateoas/pom.xml index 881626d5a..1193a2583 100644 --- a/rest-notes-spring-hateoas/pom.xml +++ b/rest-notes-spring-hateoas/pom.xml @@ -95,7 +95,7 @@ generate-docs - package + prepare-package process-asciidoc @@ -109,6 +109,27 @@ + + maven-resources-plugin + 2.7 + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/static/docs + + + ${project.build.directory}/generated-docs + + + + + + From 42270b127c0b26730e5148e8c9195055f1becd79 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 16:46:56 +0000 Subject: [PATCH 0013/1059] Improve code structure, javadoc, and build configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit makes extensive changes to the structure of the code. It introduces a number of separate packages to provide better separation of the various areas of functionality. Alongside this change the project has been renamed from spring-restdocs-core to spring-restdocs. The build has been improved to provide support for building the samples from the main build using the buildSamples task. While this change has been made, the samples remain standalone projects so that their configuration is not dependent on the main project’s build. Running buildSamples will build the samples using both Maven and Gradle. All of the main project’s classes now have javadoc and licence/copyright headers. --- build.gradle | 79 ++++- gradle/wrapper/gradle-wrapper.properties | 4 +- .../.settings/org.eclipse.jdt.core.prefs | 0 .../.settings/org.eclipse.jdt.ui.prefs | 0 .../rest-notes-spring-data-rest}/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../rest-notes-spring-data-rest}/gradlew | 0 .../rest-notes-spring-data-rest}/gradlew.bat | 0 .../rest-notes-spring-data-rest}/pom.xml | 2 +- .../src/main/asciidoc/api-guide.asciidoc | 0 .../asciidoc/getting-started-guide.asciidoc | 0 .../src/main/java/com/example/notes/Note.java | 0 .../com/example/notes/NoteRepository.java | 0 .../notes/RestNotesSpringDataRest.java | 0 .../src/main/java/com/example/notes/Tag.java | 0 .../java/com/example/notes/TagRepository.java | 0 .../com/example/notes/ApiDocumentation.java | 8 +- .../notes/GettingStartedDocumentation.java | 6 +- .../.settings/org.eclipse.jdt.core.prefs | 0 .../.settings/org.eclipse.jdt.ui.prefs | 0 .../rest-notes-spring-hateoas}/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../rest-notes-spring-hateoas}/gradlew | 0 .../rest-notes-spring-hateoas}/gradlew.bat | 0 .../rest-notes-spring-hateoas}/pom.xml | 2 +- .../src/main/asciidoc/api-guide.asciidoc | 0 .../asciidoc/getting-started-guide.asciidoc | 0 .../com/example/notes/AbstractNoteInput.java | 0 .../com/example/notes/AbstractTagInput.java | 0 .../ExceptionSupressingErrorAttributes.java | 0 .../com/example/notes/IndexController.java | 0 .../example/notes/NestedContentResource.java | 0 .../src/main/java/com/example/notes/Note.java | 0 .../java/com/example/notes/NoteInput.java | 0 .../com/example/notes/NotePatchInput.java | 0 .../com/example/notes/NoteRepository.java | 0 .../example/notes/NoteResourceAssembler.java | 0 .../com/example/notes/NotesController.java | 0 .../com/example/notes/NullOrNotBlank.java | 0 .../notes/ResourceDoesNotExistException.java | 0 .../notes/RestNotesControllerAdvice.java | 0 .../example/notes/RestNotesSpringHateoas.java | 0 .../src/main/java/com/example/notes/Tag.java | 0 .../main/java/com/example/notes/TagInput.java | 0 .../java/com/example/notes/TagPatchInput.java | 0 .../java/com/example/notes/TagRepository.java | 0 .../example/notes/TagResourceAssembler.java | 0 .../com/example/notes/TagsController.java | 0 .../src/main/resources/application.properties | 0 .../com/example/notes/ApiDocumentation.java | 8 +- .../notes/GettingStartedDocumentation.java | 6 +- settings.gradle | 4 +- .../core/RestDocumentationConfiguration.java | 65 ---- .../core/RestDocumentationResultHandler.java | 61 ---- .../core/RestDocumentationResultHandlers.java | 319 ------------------ .../.settings/org.eclipse.jdt.core.prefs | 0 .../.settings/org.eclipse.jdt.ui.prefs | 0 .../restdocs/RestDocumentation.java | 45 +++ .../restdocs/RestDocumentationConfigurer.java | 112 ++++++ .../RestDocumentationResultHandler.java | 100 ++++++ .../restdocs/curl/CurlConfiguration.java | 23 +- .../restdocs/curl/CurlDocumentation.java | 189 +++++++++++ .../curl/CurlSnippetResultHandler.java | 44 +++ .../hypermedia/HypermediaDocumentation.java | 63 ++++ .../restdocs/hypermedia}/Link.java | 2 +- .../restdocs/hypermedia}/LinkDescriptor.java | 19 +- .../restdocs/hypermedia}/LinkExtractor.java | 2 +- .../restdocs/hypermedia}/LinkExtractors.java | 10 +- .../hypermedia/LinkSnippetResultHandler.java | 113 +++++++ .../restdocs/snippet/AsciidoctorWriter.java | 47 +-- .../snippet}/DocumentationProperties.java | 4 +- .../restdocs/snippet/DocumentationWriter.java | 74 ++++ .../snippet/SnippetWritingResultHandler.java | 79 +++++ .../restdocs/util}/IterableEnumeration.java | 22 +- .../restdocs/core/LinkExtractorsTests.java | 3 + .../RestDocumentationConfigurerTests.java | 78 +++++ .../snippet/AsciidoctorWriterTests.java | 67 ++++ .../atom/multiple-links-different-rels.json | 0 .../atom/multiple-links-same-rels.json | 0 .../link-payloads/atom/no-links.json | 0 .../link-payloads/atom/single-link.json | 0 .../link-payloads/atom/wrong-format.json | 0 .../hal/multiple-links-different-rels.json | 0 .../hal/multiple-links-same-rels.json | 0 .../resources/link-payloads/hal/no-links.json | 0 .../link-payloads/hal/single-link.json | 0 .../link-payloads/hal/wrong-format.json | 0 89 files changed, 1135 insertions(+), 531 deletions(-) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/.settings/org.eclipse.jdt.core.prefs (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/.settings/org.eclipse.jdt.ui.prefs (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/build.gradle (93%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/gradle/wrapper/gradle-wrapper.jar (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/gradle/wrapper/gradle-wrapper.properties (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/gradlew (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/gradlew.bat (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/pom.xml (98%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/asciidoc/api-guide.asciidoc (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/asciidoc/getting-started-guide.asciidoc (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/java/com/example/notes/Note.java (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/java/com/example/notes/NoteRepository.java (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/java/com/example/notes/RestNotesSpringDataRest.java (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/java/com/example/notes/Tag.java (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/main/java/com/example/notes/TagRepository.java (100%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/test/java/com/example/notes/ApiDocumentation.java (97%) rename {rest-notes-spring-data-rest => samples/rest-notes-spring-data-rest}/src/test/java/com/example/notes/GettingStartedDocumentation.java (97%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/.settings/org.eclipse.jdt.core.prefs (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/.settings/org.eclipse.jdt.ui.prefs (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/build.gradle (91%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/gradle/wrapper/gradle-wrapper.jar (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/gradle/wrapper/gradle-wrapper.properties (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/gradlew (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/gradlew.bat (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/pom.xml (98%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/asciidoc/api-guide.asciidoc (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/asciidoc/getting-started-guide.asciidoc (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/AbstractNoteInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/AbstractTagInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/IndexController.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NestedContentResource.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/Note.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NoteInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NotePatchInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NoteRepository.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NoteResourceAssembler.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NotesController.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/NullOrNotBlank.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/ResourceDoesNotExistException.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/RestNotesControllerAdvice.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/RestNotesSpringHateoas.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/Tag.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/TagInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/TagPatchInput.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/TagRepository.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/TagResourceAssembler.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/java/com/example/notes/TagsController.java (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/main/resources/application.properties (100%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/test/java/com/example/notes/ApiDocumentation.java (97%) rename {rest-notes-spring-hateoas => samples/rest-notes-spring-hateoas}/src/test/java/com/example/notes/GettingStartedDocumentation.java (97%) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java rename {spring-restdocs-core => spring-restdocs}/.settings/org.eclipse.jdt.core.prefs (100%) rename {spring-restdocs-core => spring-restdocs}/.settings/org.eclipse.jdt.ui.prefs (100%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java => spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java (60%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia}/Link.java (97%) rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia}/LinkDescriptor.java (68%) rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia}/LinkExtractor.java (96%) rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia}/LinkExtractors.java (96%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java => spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java (50%) rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/snippet}/DocumentationProperties.java (93%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java rename {spring-restdocs-core/src/main/java/org/springframework/restdocs/core => spring-restdocs/src/main/java/org/springframework/restdocs/util}/IterableEnumeration.java (65%) rename {spring-restdocs-core => spring-restdocs}/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java (95%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/atom/multiple-links-different-rels.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/atom/multiple-links-same-rels.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/atom/no-links.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/atom/single-link.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/atom/wrong-format.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/hal/multiple-links-different-rels.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/hal/multiple-links-same-rels.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/hal/no-links.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/hal/single-link.json (100%) rename {spring-restdocs-core => spring-restdocs}/src/test/resources/link-payloads/hal/wrong-format.json (100%) diff --git a/build.gradle b/build.gradle index e61cbffe3..3337251d9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,33 +1,25 @@ -allprojects { +project(':spring-restdocs') { + ext { - springVersion = '4.1.1.RELEASE' + jacksonVersion = '2.3.4' + junitVersion = '4.11' + servletApiVersion = '3.1.0' + springVersion = '4.1.4.RELEASE' } + group = 'org.springframework.restdocs' -} -project(':spring-restdocs-core') { apply plugin: 'java' + apply plugin: 'eclipse' + apply plugin: 'maven' sourceCompatibility = 1.7 targetCompatibility = 1.7 - dependencies { - compile 'junit:junit:4.11' - compile "org.springframework:spring-test:$springVersion" - compile "org.springframework:spring-web:$springVersion" - compile 'javax.servlet:javax.servlet-api:3.1.0' - compile 'com.fasterxml.jackson.core:jackson-databind:2.3.4' - } -} - -subprojects { repositories { jcenter() } - apply plugin: 'eclipse' - apply plugin: 'maven' - task sourcesJar(type: Jar) { classifier = 'sources' from project.sourceSets.main.allSource @@ -45,4 +37,57 @@ subprojects { eclipseJdt.onlyIf { false } cleanEclipseJdt.onlyIf { false } + + dependencies { + compile "junit:junit:$junitVersion" + compile "org.springframework:spring-test:$springVersion" + compile "org.springframework:spring-web:$springVersion" + compile "javax.servlet:javax.servlet-api:$servletApiVersion" + compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + } +} + +task buildSamples { + description = 'Assembles and tests the sample projects using both Maven and Gradle' + group = 'Build' + dependsOn 'buildMavenSamples' + dependsOn 'buildGradleSamples' +} + +task buildMavenSamples { + dependsOn 'buildRestNotesSpringHateoasSampleWithMaven' + dependsOn 'buildRestNotesSpringDataRestSampleWithMaven' +} + +task buildGradleSamples { + dependsOn 'buildRestNotesSpringHateoasSampleWithGradle' + dependsOn 'buildRestNotesSpringDataRestSampleWithGradle' +} + +task buildRestNotesSpringHateoasSampleWithGradle(type: GradleBuild) { + dependsOn 'spring-restdocs:install' + dir = 'samples/rest-notes-spring-hateoas' + tasks = ['clean', 'build'] +} + +task buildRestNotesSpringDataRestSampleWithGradle(type: GradleBuild) { + dependsOn 'spring-restdocs:install' + dir = 'samples/rest-notes-spring-data-rest' + tasks = ['clean', 'build'] +} + +task buildRestNotesSpringHateoasSampleWithMaven(type: Exec) { + dependsOn 'spring-restdocs:install' + workingDir 'samples/rest-notes-spring-hateoas' + commandLine 'mvn', 'clean', 'package' +} + +task buildRestNotesSpringDataRestSampleWithMaven(type: Exec) { + dependsOn 'spring-restdocs:install' + workingDir 'samples/rest-notes-spring-data-rest' + commandLine 'mvn', 'clean', 'package' +} + +wrapper { + gradleVersion = '2.2' } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a89525099..0cf48989c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 08 10:04:39 BST 2014 +#Mon Feb 16 13:50:18 GMT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-bin.zip diff --git a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs rename to samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs diff --git a/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs rename to samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs diff --git a/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle similarity index 93% rename from rest-notes-spring-data-rest/build.gradle rename to samples/rest-notes-spring-data-rest/build.gradle index fa6262c94..21ba3459c 100644 --- a/rest-notes-spring-data-rest/build.gradle +++ b/samples/rest-notes-spring-data-rest/build.gradle @@ -31,7 +31,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' } diff --git a/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar rename to samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar diff --git a/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties rename to samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties diff --git a/rest-notes-spring-data-rest/gradlew b/samples/rest-notes-spring-data-rest/gradlew similarity index 100% rename from rest-notes-spring-data-rest/gradlew rename to samples/rest-notes-spring-data-rest/gradlew diff --git a/rest-notes-spring-data-rest/gradlew.bat b/samples/rest-notes-spring-data-rest/gradlew.bat similarity index 100% rename from rest-notes-spring-data-rest/gradlew.bat rename to samples/rest-notes-spring-data-rest/gradlew.bat diff --git a/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml similarity index 98% rename from rest-notes-spring-data-rest/pom.xml rename to samples/rest-notes-spring-data-rest/pom.xml index 37b10e6b8..1cc246de8 100644 --- a/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -48,7 +48,7 @@ org.springframework.restdocs - spring-restdocs-core + spring-restdocs 0.1.0.BUILD-SNAPSHOT test diff --git a/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc similarity index 100% rename from rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc rename to samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc diff --git a/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc similarity index 100% rename from rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc rename to samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java similarity index 100% rename from rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java rename to samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java similarity index 100% rename from rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java rename to samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java similarity index 100% rename from rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java rename to samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java similarity index 100% rename from rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java rename to samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java diff --git a/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java similarity index 100% rename from rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java rename to samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java diff --git a/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 similarity index 97% rename from rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java rename to samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index c7ae09466..55fe34e85 100644 --- a/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 @@ -18,8 +18,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 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; @@ -39,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.core.RestDocumentationConfiguration; +import org.springframework.restdocs.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -70,7 +70,7 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration()).build(); + .apply(new RestDocumentationConfigurer()).build(); } @Test diff --git a/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java similarity index 97% rename from rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java rename to samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 8687ff2ad..ec54ff5d2 100644 --- a/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.core.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.document; 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; @@ -38,7 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.core.RestDocumentationConfiguration; +import org.springframework.restdocs.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -66,7 +66,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration()).build(); + .apply(new RestDocumentationConfigurer()).build(); } @Test diff --git a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs rename to samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs diff --git a/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs rename to samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs diff --git a/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle similarity index 91% rename from rest-notes-spring-hateoas/build.gradle rename to samples/rest-notes-spring-hateoas/build.gradle index fff6d0e1f..6f324f815 100644 --- a/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -8,7 +8,7 @@ buildscript { } plugins { - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.convert" version "1.5.2" } apply plugin: 'java' @@ -34,7 +34,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' } ext { diff --git a/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar rename to samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar diff --git a/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties rename to samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties diff --git a/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew similarity index 100% rename from rest-notes-spring-hateoas/gradlew rename to samples/rest-notes-spring-hateoas/gradlew diff --git a/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat similarity index 100% rename from rest-notes-spring-hateoas/gradlew.bat rename to samples/rest-notes-spring-hateoas/gradlew.bat diff --git a/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml similarity index 98% rename from rest-notes-spring-hateoas/pom.xml rename to samples/rest-notes-spring-hateoas/pom.xml index 1193a2583..1c9bfbbaa 100644 --- a/rest-notes-spring-hateoas/pom.xml +++ b/samples/rest-notes-spring-hateoas/pom.xml @@ -64,7 +64,7 @@ org.springframework.restdocs - spring-restdocs-core + spring-restdocs 0.1.0.BUILD-SNAPSHOT test diff --git a/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc similarity index 100% rename from rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc rename to samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc diff --git a/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc similarity index 100% rename from rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc rename to samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java diff --git a/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java similarity index 100% rename from rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java rename to samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java diff --git a/rest-notes-spring-hateoas/src/main/resources/application.properties b/samples/rest-notes-spring-hateoas/src/main/resources/application.properties similarity index 100% rename from rest-notes-spring-hateoas/src/main/resources/application.properties rename to samples/rest-notes-spring-hateoas/src/main/resources/application.properties diff --git a/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 similarity index 97% rename from rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java rename to samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 7e201ed2b..d82451c84 100644 --- a/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 @@ -18,8 +18,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.core.RestDocumentation.document; -import static org.springframework.restdocs.core.RestDocumentation.linkWithRel; +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; 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; @@ -39,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.core.RestDocumentationConfiguration; +import org.springframework.restdocs.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -70,7 +70,7 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration()).build(); + .apply(new RestDocumentationConfigurer()).build(); } @Test diff --git a/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java similarity index 97% rename from rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java rename to samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index a37e573e1..49892b521 100644 --- a/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.core.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.document; 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; @@ -39,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.core.RestDocumentationConfiguration; +import org.springframework.restdocs.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -67,7 +67,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration()).build(); + .apply(new RestDocumentationConfigurer()).build(); } @Test diff --git a/settings.gradle b/settings.gradle index c74ec6c26..80e754ca7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ -rootProject.name = 'spring-restdocs' +rootProject.name = 'spring-restdocs-build' -include 'spring-restdocs-core' \ No newline at end of file +include 'spring-restdocs' \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java deleted file mode 100644 index 70a7c70ed..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2014 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.core; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.test.web.servlet.request.RequestPostProcessor; -import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; -import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.web.context.WebApplicationContext; - -public class RestDocumentationConfiguration extends MockMvcConfigurerAdapter { - - private String scheme = "http"; - - private String host = "localhost"; - - private int port = 8080; - - public RestDocumentationConfiguration withScheme(String scheme) { - this.scheme = scheme; - return this; - } - - public RestDocumentationConfiguration withHost(String host) { - this.host = host; - return this; - } - - public RestDocumentationConfiguration withPort(int port) { - this.port = port; - return this; - } - - @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new RequestPostProcessor() { - - @Override - public MockHttpServletRequest postProcessRequest( - MockHttpServletRequest request) { - request.setScheme(RestDocumentationConfiguration.this.scheme); - request.setRemotePort(RestDocumentationConfiguration.this.port); - request.setServerPort(RestDocumentationConfiguration.this.port); - request.setRemoteHost(RestDocumentationConfiguration.this.host); - return request; - } - }; - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java deleted file mode 100644 index a5464bfd5..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandler.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 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.core; - -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse; - -import java.util.Arrays; - -import org.springframework.restdocs.core.RestDocumentationResultHandlers.LinkDocumentingResultHandler; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultHandler; - -public class RestDocumentationResultHandler implements ResultHandler { - - private final String outputDir; - - private ResultHandler linkDocumentingResultHandler; - - public RestDocumentationResultHandler(String outputDir) { - this.outputDir = outputDir; - } - - @Override - public void handle(MvcResult result) throws Exception { - documentCurlRequest(this.outputDir).includeResponseHeaders().handle(result); - documentCurlResponse(this.outputDir).includeResponseHeaders().handle(result); - documentCurlRequestAndResponse(this.outputDir).includeResponseHeaders().handle( - result); - if (this.linkDocumentingResultHandler != null) { - this.linkDocumentingResultHandler.handle(result); - } - } - - public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { - return withLinks(null, descriptors); - } - - public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, - LinkDescriptor... descriptors) { - this.linkDocumentingResultHandler = new LinkDocumentingResultHandler( - this.outputDir, linkExtractor, Arrays.asList(descriptors)); - return this; - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java deleted file mode 100644 index 7b066cc28..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ /dev/null @@ -1,319 +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.core; - -import static org.junit.Assert.fail; -import static org.springframework.restdocs.core.IterableEnumeration.iterable; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.core.DocumentationWriter.DocumentationAction; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultHandler; -import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; -import org.springframework.web.bind.annotation.RequestMethod; - -public abstract class RestDocumentationResultHandlers { - - public static CurlResultHandler documentCurlRequest(String outputDir) { - return new CurlResultHandler(outputDir, "request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - } - }; - } - - public static CurlResultHandler documentCurlResponse(String outputDir) { - return new CurlResultHandler(outputDir, "response") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); - } - }; - } - - public static CurlResultHandler documentCurlRequestAndResponse(String outputDir) { - return new CurlResultHandler(outputDir, "request-response") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); - } - }; - } - - private static final class CurlRequestDocumentationAction implements - DocumentationAction { - - private final DocumentationWriter writer; - - private final MvcResult result; - - private final CurlConfiguration curlConfiguration; - - CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { - this.writer = writer; - this.result = result; - this.curlConfiguration = curlConfiguration; - } - - @Override - public void perform() throws Exception { - MockHttpServletRequest request = this.result.getRequest(); - this.writer.print(String.format("curl %s://%s:%d%s", request.getScheme(), - request.getRemoteHost(), request.getRemotePort(), - request.getRequestURI())); - - if (this.curlConfiguration.includeResponseHeaders) { - this.writer.print(" -i"); - } - - RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod()); - if (requestMethod != RequestMethod.GET) { - this.writer.print(String.format(" -X %s", requestMethod.toString())); - } - - for (String headerName : iterable(request.getHeaderNames())) { - for (String header : iterable(request.getHeaders(headerName))) { - this.writer - .print(String.format(" -H \"%s: %s\"", headerName, header)); - } - } - - if (request.getContentLengthLong() > 0) { - this.writer.print(String.format(" -d '%s'", getContent(request))); - } - - this.writer.println(); - } - - private String getContent(MockHttpServletRequest request) throws IOException { - StringWriter writer = new StringWriter(); - FileCopyUtils.copy(request.getReader(), writer); - return writer.toString(); - } - } - - private static final class CurlResponseDocumentationAction implements - DocumentationAction { - - private final DocumentationWriter writer; - - private final MvcResult result; - - private final CurlConfiguration curlConfiguration; - - CurlResponseDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { - this.writer = writer; - this.result = result; - this.curlConfiguration = curlConfiguration; - } - - @Override - public void perform() throws Exception { - if (this.curlConfiguration.includeResponseHeaders) { - HttpStatus status = HttpStatus.valueOf(this.result.getResponse() - .getStatus()); - this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), - status.getReasonPhrase())); - for (String headerName : this.result.getResponse().getHeaderNames()) { - for (String header : this.result.getResponse().getHeaders(headerName)) { - this.writer.println(String.format("%s: %s", headerName, header)); - } - } - this.writer.println(); - } - this.writer.println(this.result.getResponse().getContentAsString()); - } - } - - private static class CurlConfiguration { - - private boolean includeResponseHeaders = false; - - } - - public static abstract class RestDocumentationResultHandler implements ResultHandler { - - private String outputDir; - - private String fileName; - - public RestDocumentationResultHandler(String outputDir, String fileName) { - this.outputDir = outputDir; - this.fileName = fileName; - } - - abstract void handle(MvcResult result, DocumentationWriter writer) - throws Exception; - - @Override - public void handle(MvcResult result) throws Exception { - PrintStream printStream = createPrintStream(); - try { - handle(result, new DocumentationWriter(printStream)); - } - finally { - printStream.close(); - } - } - - protected PrintStream createPrintStream() throws FileNotFoundException { - - File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); - if (!outputFile.isAbsolute()) { - outputFile = makeAbsolute(outputFile); - } - - if (outputFile != null) { - outputFile.getParentFile().mkdirs(); - return new PrintStream(new FileOutputStream(outputFile)); - } - - return System.out; - } - - private static File makeAbsolute(File outputFile) { - File outputDir = new DocumentationProperties().getOutputDir(); - if (outputDir != null) { - return new File(outputDir, outputFile.getPath()); - } - return null; - } - } - - public static abstract class CurlResultHandler extends RestDocumentationResultHandler { - - private final CurlConfiguration curlConfiguration = new CurlConfiguration(); - - public CurlResultHandler(String outputDir, String fileName) { - super(outputDir, fileName); - } - - CurlConfiguration getCurlConfiguration() { - return this.curlConfiguration; - } - - public CurlResultHandler includeResponseHeaders() { - this.curlConfiguration.includeResponseHeaders = true; - return this; - } - } - - static class LinkDocumentingResultHandler extends RestDocumentationResultHandler { - - private final Map descriptorsByRel = new HashMap(); - - private final LinkExtractor extractor; - - public LinkDocumentingResultHandler(String outputDir, - LinkExtractor linkExtractor, List descriptors) { - super(outputDir, "links"); - this.extractor = linkExtractor; - for (LinkDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getRel()); - Assert.hasText(descriptor.getDescription()); - this.descriptorsByRel.put(descriptor.getRel(), descriptor); - } - } - - @Override - void handle(MvcResult result, DocumentationWriter writer) throws Exception { - Map> links; - if (this.extractor != null) { - links = this.extractor.extractLinks(result.getResponse()); - } - else { - String contentType = result.getResponse().getContentType(); - LinkExtractor extractorForContentType = LinkExtractors - .extractorForContentType(contentType); - if (extractorForContentType != null) { - links = extractorForContentType.extractLinks(result.getResponse()); - } - else { - throw new IllegalStateException( - "No LinkExtractor has been provided and one is not available for the content type " - + contentType); - } - - } - - Set actualRels = links.keySet(); - Set expectedRels = this.descriptorsByRel.keySet(); - - Set undocumentedRels = new HashSet(actualRels); - undocumentedRels.removeAll(expectedRels); - - Set missingRels = new HashSet(expectedRels); - missingRels.removeAll(actualRels); - - if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { - String message = ""; - if (!undocumentedRels.isEmpty()) { - message += "Links with the following relations were not documented: " - + undocumentedRels; - } - if (!missingRels.isEmpty()) { - message += "Links with the following relations were not found in the response: " - + missingRels; - } - fail(message); - } - - Assert.isTrue(actualRels.equals(expectedRels)); - - writer.println("|==="); - writer.println("| Relation | Description"); - - for (Entry entry : this.descriptorsByRel.entrySet()) { - writer.println(); - writer.println("| " + entry.getKey()); - writer.println("| " + entry.getValue().getDescription()); - } - - writer.println("|==="); - } - - } -} diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs rename to spring-restdocs/.settings/org.eclipse.jdt.core.prefs diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs rename to spring-restdocs/.settings/org.eclipse.jdt.ui.prefs diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java new file mode 100644 index 000000000..87cd79e4f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -0,0 +1,45 @@ +/* + * 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; + +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +/** + * Static factory methods for documenting RESTful APIs using Spring MVC Test + * + * @author Andy Wilkinson + */ +public abstract class RestDocumentation { + + private RestDocumentation() { + + } + + /** + * Documents the API call to the given {@code outputDir}. + * + * @param outputDir The directory to which the documentation will be written + * @return a Mock MVC {@code ResultHandler} that will produce the documentation + * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) + * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) + */ + public static RestDocumentationResultHandler document(String outputDir) { + return new RestDocumentationResultHandler(outputDir); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java new file mode 100644 index 000000000..f644cbbd3 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java @@ -0,0 +1,112 @@ +/* + * 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; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; +import org.springframework.web.context.WebApplicationContext; + +/** + * A {@link MockMvcConfigurer} that can be used to configure the documentation + * + * @author Andy Wilkinson + * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * + */ +public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { + + /** + * The default scheme for documented URIs + * @see #withScheme(String) + */ + public static final String DEFAULT_SCHEME = "http"; + + /** + * The defalt host for documented URIs + * @see #withHost(String) + */ + public static final String DEFAULT_HOST = "localhost"; + + /** + * The default port for documented URIs + * @see #withPort(int) + */ + public static final int DEFAULT_PORT = 8080; + + private String scheme = DEFAULT_SCHEME; + + private String host = DEFAULT_HOST; + + private int port = DEFAULT_PORT; + + /** + * Configures any documented URIs to use the given {@code scheme}. The default is + * {@code http}. + * + * @param scheme The URI scheme + * @return {@code this} + */ + public RestDocumentationConfigurer withScheme(String scheme) { + this.scheme = scheme; + return this; + } + + /** + * Configures any documented URIs to use the given {@code host}. The default is + * {@code localhost}. + * + * @param host The URI host + * @return {@code this} + */ + public RestDocumentationConfigurer withHost(String host) { + this.host = host; + return this; + } + + /** + * Configures any documented URIs to use the given {@code port}. The default is + * {@code 8080}. + * + * @param port The URI port + * @return {@code this} + */ + public RestDocumentationConfigurer withPort(int port) { + this.port = port; + return this; + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return new RequestPostProcessor() { + + @Override + public MockHttpServletRequest postProcessRequest( + MockHttpServletRequest request) { + request.setScheme(RestDocumentationConfigurer.this.scheme); + request.setRemotePort(RestDocumentationConfigurer.this.port); + request.setServerPort(RestDocumentationConfigurer.this.port); + request.setRemoteHost(RestDocumentationConfigurer.this.host); + return request; + } + }; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java new file mode 100644 index 000000000..571f2bec1 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -0,0 +1,100 @@ +/* + * 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; + +import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; +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 java.util.ArrayList; +import java.util.List; + +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.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; + +/** + * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. + * + * @author Andy Wilkinson + * @see RestDocumentation#document(String) + */ +public class RestDocumentationResultHandler implements ResultHandler { + + private final String outputDir; + + private List delegates; + + RestDocumentationResultHandler(String outputDir) { + this.outputDir = outputDir; + + this.delegates = new ArrayList(); + this.delegates.add(documentCurlRequest(this.outputDir)); + this.delegates.add(documentCurlResponse(this.outputDir)); + this.delegates.add(documentCurlRequestAndResponse(this.outputDir)); + } + + @Override + public void handle(MvcResult result) throws Exception { + for (ResultHandler delegate : this.delegates) { + delegate.handle(result); + } + } + + /** + * Document the links in the response using the given {@code descriptors}. The links + * are extracted from the response based on its content type. + *

+ * If a link 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 link is + * described but is not present in the response a failure will also occur when this + * handler is invoked. + * + * @param descriptors the link descriptors + * @return {@code this} + * @see HypermediaDocumentation#linkWithRel(String) + * @see LinkExtractors#extractorForContentType(String) + */ + public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { + return withLinks(null, descriptors); + } + + /** + * Document the links in the response using the given {@code descriptors}. The links + * are extracted from the response using the given {@code linkExtractor}. + *

+ * If a link 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 link is + * described but is not present in the response a failure will also occur when this + * handler is invoked. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the link descriptors + * @return {@code this} + * @see HypermediaDocumentation#linkWithRel(String) + */ + public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + this.delegates.add(documentLinks(this.outputDir, linkExtractor, descriptors)); + return this; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java similarity index 60% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java index 974e1dbb6..c8c8c9441 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java @@ -14,17 +14,22 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.curl; -public class RestDocumentation { +/** + * Configuration for documenting Curl requests and responses + * + * @author Andy Wilkinson + */ +class CurlConfiguration { - public static RestDocumentationResultHandler document(String outputDir) - throws Exception { - return new RestDocumentationResultHandler(outputDir); - } + private boolean includeResponseHeaders = true; - public static LinkDescriptor linkWithRel(String rel) { - return new LinkDescriptor(rel); + boolean isIncludeResponseHeaders() { + return this.includeResponseHeaders; } -} + void setIncludeResponseHeaders(boolean includeResponseHeaders) { + this.includeResponseHeaders = includeResponseHeaders; + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java new file mode 100644 index 000000000..820058760 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -0,0 +1,189 @@ +/* + * 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.curl; + +import static org.springframework.restdocs.util.IterableEnumeration.iterable; + +import java.io.IOException; +import java.io.StringWriter; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.snippet.DocumentationWriter; +import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the cURL command-line utility. + * + * @author Andy Wilkinson + */ +public abstract class CurlDocumentation { + + private CurlDocumentation() { + + } + + /** + * Produces a documentation snippet containing the request formatted as a cURL command + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static CurlSnippetResultHandler documentCurlRequest(String outputDir) { + return new CurlSnippetResultHandler(outputDir, "request") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws Exception { + writer.shellCommand(new CurlRequestDocumentationAction(writer, result, + getCurlConfiguration())); + } + }; + } + + /** + * Produces a documentation snippet containing the response formatted as the response + * to a cURL command + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static CurlSnippetResultHandler documentCurlResponse(String outputDir) { + return new CurlSnippetResultHandler(outputDir, "response") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws Exception { + writer.codeBlock("http", new CurlResponseDocumentationAction(writer, + result, getCurlConfiguration())); + } + }; + } + + /** + * Produces a documentation snippet containing both the request formatted as a cURL + * command and the response formatted formatted s the response to a cURL command. + * + * @param outputDir The directory to which the snippet should be written + * @return the handler that will produce the snippet + */ + public static CurlSnippetResultHandler documentCurlRequestAndResponse(String outputDir) { + return new CurlSnippetResultHandler(outputDir, "request-response") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws Exception { + writer.shellCommand(new CurlRequestDocumentationAction(writer, result, + getCurlConfiguration())); + writer.codeBlock("http", new CurlResponseDocumentationAction(writer, + result, getCurlConfiguration())); + } + }; + } + + private static final class CurlRequestDocumentationAction implements + DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + private final CurlConfiguration curlConfiguration; + + CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result, + CurlConfiguration curlConfiguration) { + this.writer = writer; + this.result = result; + this.curlConfiguration = curlConfiguration; + } + + @Override + public void perform() throws Exception { + MockHttpServletRequest request = this.result.getRequest(); + this.writer.print(String.format("curl %s://%s:%d%s", request.getScheme(), + request.getRemoteHost(), request.getRemotePort(), + request.getRequestURI())); + + if (this.curlConfiguration.isIncludeResponseHeaders()) { + this.writer.print(" -i"); + } + + RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod()); + if (requestMethod != RequestMethod.GET) { + this.writer.print(String.format(" -X %s", requestMethod.toString())); + } + + for (String headerName : iterable(request.getHeaderNames())) { + for (String header : iterable(request.getHeaders(headerName))) { + this.writer + .print(String.format(" -H \"%s: %s\"", headerName, header)); + } + } + + if (request.getContentLengthLong() > 0) { + this.writer.print(String.format(" -d '%s'", getContent(request))); + } + + this.writer.println(); + } + + private String getContent(MockHttpServletRequest request) throws IOException { + StringWriter bodyWriter = new StringWriter(); + FileCopyUtils.copy(request.getReader(), bodyWriter); + return bodyWriter.toString(); + } + } + + private static final class CurlResponseDocumentationAction implements + DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + private final CurlConfiguration curlConfiguration; + + CurlResponseDocumentationAction(DocumentationWriter writer, MvcResult result, + CurlConfiguration curlConfiguration) { + this.writer = writer; + this.result = result; + this.curlConfiguration = curlConfiguration; + } + + @Override + public void perform() throws Exception { + if (this.curlConfiguration.isIncludeResponseHeaders()) { + HttpStatus status = HttpStatus.valueOf(this.result.getResponse() + .getStatus()); + this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), + status.getReasonPhrase())); + for (String headerName : this.result.getResponse().getHeaderNames()) { + for (String header : this.result.getResponse().getHeaders(headerName)) { + this.writer.println(String.format("%s: %s", headerName, header)); + } + } + this.writer.println(); + } + this.writer.println(this.result.getResponse().getContentAsString()); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java new file mode 100644 index 000000000..31a687cf7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java @@ -0,0 +1,44 @@ +/* + * 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.curl; + +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.test.web.servlet.ResultHandler; + +/** + * Abstract base class for Spring Mock MVC {@link ResultHandler ResultHandlers} that + * produce documentation snippets relating to cURL. + * + * @author Andy Wilkinson + */ +public abstract class CurlSnippetResultHandler extends SnippetWritingResultHandler { + + private final CurlConfiguration curlConfiguration = new CurlConfiguration(); + + public CurlSnippetResultHandler(String outputDir, String fileName) { + super(outputDir, fileName); + } + + CurlConfiguration getCurlConfiguration() { + return this.curlConfiguration; + } + + public CurlSnippetResultHandler includeResponseHeaders() { + this.curlConfiguration.setIncludeResponseHeaders(true); + return this; + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java new file mode 100644 index 000000000..65a51bd09 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -0,0 +1,63 @@ +/* + * 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.hypermedia; + +import java.util.Arrays; + +import org.springframework.restdocs.RestDocumentationResultHandler; + +/** + * Static factory methods for documenting a RESTful API that utilises Hypermedia. + * + * @author Andy Wilkinson + */ +public abstract class HypermediaDocumentation { + + private HypermediaDocumentation() { + + } + + /** + * Creates a {@code LinkDescriptor} that describes a link with the given {@code rel}. + * + * @param rel The rel of the link + * @return a {@code LinkDescriptor} ready for further configuration + * @see RestDocumentationResultHandler#withLinks(LinkDescriptor...) + * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) + */ + public static LinkDescriptor linkWithRel(String rel) { + return new LinkDescriptor(rel); + } + + /** + * Creates a {@code LinkSnippetResultHandler} that will produce a documentation + * snippet for a response's links. + * + * @param outputDir The directory to which the snippet should be written + * @param linkExtractor Used to extract the links from the response + * @param descriptors The descriptions of the response's links + * @return the handler + * @see RestDocumentationResultHandler#withLinks(LinkDescriptor...) + * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) + */ + public static LinkSnippetResultHandler documentLinks(String outputDir, + LinkExtractor linkExtractor, LinkDescriptor... descriptors) { + return new LinkSnippetResultHandler(outputDir, linkExtractor, + Arrays.asList(descriptors)); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/Link.java similarity index 97% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/Link.java index ef1534878..694eec9e7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/Link.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.hypermedia; import org.springframework.core.style.ToStringCreator; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java similarity index 68% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index e28a8af4c..24a2044cc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -14,18 +14,31 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.hypermedia; +/** + * A description of a link found in a hypermedia API + * + * @see HypermediaDocumentation#linkWithRel(String) + * + * @author Andy Wilkinson + */ public class LinkDescriptor { private final String rel; private String description; - public LinkDescriptor(String rel) { + LinkDescriptor(String rel) { this.rel = rel; } + /** + * Specifies the description of the link + * + * @param description The link's description + * @return {@code this} + */ public LinkDescriptor description(String description) { this.description = description; return this; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java similarity index 96% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java index f63d2c4f3..0627a306c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.hypermedia; import java.io.IOException; import java.util.List; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java similarity index 96% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java index df35102e6..07ffd203c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/LinkExtractors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.hypermedia; import java.io.IOException; import java.util.ArrayList; @@ -35,7 +35,11 @@ * * @author Andy Wilkinson */ -public class LinkExtractors { +public abstract class LinkExtractors { + + private LinkExtractors() { + + } /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia @@ -75,7 +79,7 @@ else if ("application/hal+json".equals(contentType)) { return null; } - private static abstract class JsonContentLinkExtractor implements LinkExtractor { + private abstract static class JsonContentLinkExtractor implements LinkExtractor { private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java new file mode 100644 index 000000000..ee8fc828c --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -0,0 +1,113 @@ +/* + * 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.hypermedia; + +import static org.junit.Assert.fail; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +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 links. + * + * @author Andy Wilkinson + */ +public class LinkSnippetResultHandler extends SnippetWritingResultHandler { + + private final Map descriptorsByRel = new HashMap(); + + private final LinkExtractor extractor; + + LinkSnippetResultHandler(String outputDir, LinkExtractor linkExtractor, + List descriptors) { + super(outputDir, "links"); + this.extractor = linkExtractor; + for (LinkDescriptor descriptor : descriptors) { + Assert.hasText(descriptor.getRel()); + Assert.hasText(descriptor.getDescription()); + this.descriptorsByRel.put(descriptor.getRel(), descriptor); + } + } + + @Override + protected void handle(MvcResult result, DocumentationWriter writer) throws Exception { + Map> links; + if (this.extractor != null) { + links = this.extractor.extractLinks(result.getResponse()); + } + else { + String contentType = result.getResponse().getContentType(); + LinkExtractor extractorForContentType = LinkExtractors + .extractorForContentType(contentType); + if (extractorForContentType != null) { + links = extractorForContentType.extractLinks(result.getResponse()); + } + else { + throw new IllegalStateException( + "No LinkExtractor has been provided and one is not available for the content type " + + contentType); + } + + } + + Set actualRels = links.keySet(); + Set expectedRels = this.descriptorsByRel.keySet(); + + Set undocumentedRels = new HashSet(actualRels); + undocumentedRels.removeAll(expectedRels); + + Set missingRels = new HashSet(expectedRels); + missingRels.removeAll(actualRels); + + if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { + String message = ""; + if (!undocumentedRels.isEmpty()) { + message += "Links with the following relations were not documented: " + + undocumentedRels; + } + if (!missingRels.isEmpty()) { + message += "Links with the following relations were not found in the response: " + + missingRels; + } + fail(message); + } + + Assert.isTrue(actualRels.equals(expectedRels)); + + writer.println("|==="); + writer.println("| Relation | Description"); + + for (Entry entry : this.descriptorsByRel.entrySet()) { + writer.println(); + writer.println("| " + entry.getKey()); + writer.println("| " + entry.getValue().getDescription()); + } + + writer.println("|==="); + } + +} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java similarity index 50% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java index 178bb37b9..5791694b2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationWriter.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -14,47 +14,48 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.snippet; -import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.Writer; -public class DocumentationWriter extends PrintWriter { - - public DocumentationWriter(OutputStream stream) { - super(stream, true); +/** + * A {@link DocumentationWriter} that produces output in Asciidoctor. + * + * @author Andy Wilkinson + */ +public class AsciidoctorWriter extends DocumentationWriter { + + /** + * Creates a new {@code AsciidoctorWriter} that will write to the given {@code writer} + * @param writer The writer to which output will be written + */ + public AsciidoctorWriter(Writer writer) { + super(writer); } - public void shellCommand(final DocumentationAction... actions) throws Exception { + @Override + public void shellCommand(final DocumentationAction action) throws Exception { codeBlock("bash", new DocumentationAction() { @Override public void perform() throws Exception { - DocumentationWriter.this.print("$ "); - for (DocumentationAction action : actions) { - action.perform(); - } + AsciidoctorWriter.this.print("$ "); + action.perform(); } }); } - public void codeBlock(String language, DocumentationAction... actions) - throws Exception { + @Override + public void codeBlock(String language, DocumentationAction action) throws Exception { println(); if (language != null) { println("[source," + language + "]"); } println("----"); - for (DocumentationAction action : actions) { - action.perform(); - } + action.perform(); println("----"); println(); } - public interface DocumentationAction { - - void perform() throws Exception; - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java similarity index 93% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java index fe644def6..f8ca87b12 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationProperties.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.snippet; import java.io.File; import java.io.IOException; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java new file mode 100644 index 000000000..83d0ee01f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java @@ -0,0 +1,74 @@ +/* + * 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.snippet; + +import java.io.PrintWriter; +import java.io.Writer; + +/** + * A {@link PrintWriter} that provides additional methods which are useful for producing + * API documentation. + * + * @author Andy Wilkinson + */ +public abstract class DocumentationWriter extends PrintWriter { + + protected DocumentationWriter(Writer writer) { + super(writer, true); + } + + /** + * Calls the given {@code action} to document a shell command. Any prefix necessary + * for the documentation format is written prior to calling the {@code action}. Having + * called the action, any necessary suffix is then written. + * + * @param action the action that will produce the shell command + * @throws Exception if the documentation fails + */ + public abstract void shellCommand(DocumentationAction action) throws Exception; + + /** + * Calls the given {@code action} to document a code block. The code block will be + * annotated as containing code written in the given {@code language}. Any prefix + * necessary for the documentation format is written prior to calling the + * {@code action}. Having called the action, any necessary suffix is the written. + * + * @param language the language in which the code is written + * @param action the action that will produce the code + * @throws Exception if the documentation fails + */ + public abstract void codeBlock(String language, DocumentationAction action) + throws Exception; + + /** + * Encapsulates an action that outputs some documentation. Typically implemented as a + * lamda or, pre-Java 8, as an anonymous inner class. + * + * @author Andy Wilkinson + * @see DocumentationWriter#shellCommand + * @see DocumentationWriter#codeBlock + */ + public interface DocumentationAction { + + /** + * Perform the encapsulated action + * + * @throws Exception if the action fails + */ + void perform() throws Exception; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java new file mode 100644 index 000000000..5785cc8d9 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -0,0 +1,79 @@ +/* + * 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.snippet; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; + +/** + * Base class for a {@link ResultHandler} that writes a documentation snippet + * + * @author Andy Wilkinson + */ +public abstract class SnippetWritingResultHandler implements ResultHandler { + + private String outputDir; + + private String fileName; + + protected SnippetWritingResultHandler(String outputDir, String fileName) { + this.outputDir = outputDir; + this.fileName = fileName; + } + + protected abstract void handle(MvcResult result, DocumentationWriter writer) + throws Exception; + + @Override + public void handle(MvcResult result) throws Exception { + Writer writer = createWriter(); + try { + handle(result, new AsciidoctorWriter(writer)); + } + finally { + writer.close(); + } + } + + private Writer createWriter() throws IOException { + File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); + if (!outputFile.isAbsolute()) { + outputFile = makeRelativeToConfiguredOutputDir(outputFile); + } + + if (outputFile != null) { + outputFile.getParentFile().mkdirs(); + return new FileWriter(outputFile); + } + + return new OutputStreamWriter(System.out); + } + + private File makeRelativeToConfiguredOutputDir(File outputFile) { + File configuredOutputDir = new DocumentationProperties().getOutputDir(); + if (configuredOutputDir != null) { + return new File(configuredOutputDir, outputFile.getPath()); + } + return null; + } +} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/IterableEnumeration.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java similarity index 65% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/core/IterableEnumeration.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java index b374fbc00..0dc04bb11 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/IterableEnumeration.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -14,16 +14,23 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.util; import java.util.Enumeration; import java.util.Iterator; -public final class IterableEnumeration implements Iterable { +/** + * An adapter to expose an {@link Enumeration} as an {@link Iterable}. + * + * @author Andy Wilkinson + * + * @param the type of the Enumeration's contents + */ +public class IterableEnumeration implements Iterable { private final Enumeration enumeration; - public IterableEnumeration(Enumeration enumeration) { + private IterableEnumeration(Enumeration enumeration) { this.enumeration = enumeration; } @@ -49,6 +56,13 @@ public void remove() { }; } + /** + * Creates a new {@code Iterable} that will iterate over the given {@code enumeration} + * + * @param the type of the enumeration's elements + * @param enumeration The enumeration to expose as an {@code Iterable} + * @return the iterable + */ public static Iterable iterable(Enumeration enumeration) { return new IterableEnumeration(enumeration); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java similarity index 95% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java index 1794b7d19..f4a77439e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java @@ -34,6 +34,9 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.hypermedia.Link; +import org.springframework.restdocs.hypermedia.LinkExtractor; +import org.springframework.restdocs.hypermedia.LinkExtractors; import org.springframework.util.FileCopyUtils; /** diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java new file mode 100644 index 000000000..5ff872ba7 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java @@ -0,0 +1,78 @@ +/* + * 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.core; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +/** + * Tests for {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationConfigurerTests { + + private MockHttpServletRequest request = new MockHttpServletRequest(); + + @Test + public void defaultConfiguration() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + .beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + + assertUriConfiguration("http", "localhost", 8080); + } + + @Test + public void customScheme() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + .withScheme("https").beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + + assertUriConfiguration("https", "localhost", 8080); + } + + @Test + public void customHost() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withHost( + "api.example.com").beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + + assertUriConfiguration("http", "api.example.com", 8080); + } + + @Test + public void customPort() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( + 8081).beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + + assertUriConfiguration("http", "localhost", 8081); + } + + private void assertUriConfiguration(String scheme, String host, int port) { + assertEquals(scheme, this.request.getScheme()); + assertEquals(host, this.request.getRemoteHost()); + assertEquals(port, this.request.getRemotePort()); + assertEquals(port, this.request.getServerPort()); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java new file mode 100644 index 000000000..33831d0b6 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java @@ -0,0 +1,67 @@ +/* + * 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.snippet; + +import static org.junit.Assert.assertEquals; + +import java.io.StringWriter; +import java.io.Writer; + +import org.junit.Test; +import org.springframework.restdocs.snippet.AsciidoctorWriter; +import org.springframework.restdocs.snippet.DocumentationWriter; +import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; + +/** + * Tests for {@link AsciidoctorWriter} + * + * @author Andy Wilkinson + */ +public class AsciidoctorWriterTests { + + private Writer output = new StringWriter(); + + private DocumentationWriter documentationWriter = new AsciidoctorWriter(this.output); + + @Test + public void codeBlock() throws Exception { + this.documentationWriter.codeBlock("java", new DocumentationAction() { + + @Override + public void perform() throws Exception { + AsciidoctorWriterTests.this.documentationWriter.println("foo"); + } + }); + + String expectedOutput = String.format("\n[source,java]\n----\nfoo\n----\n\n"); + assertEquals(expectedOutput, this.output.toString()); + } + + @Test + public void shellCommand() throws Exception { + this.documentationWriter.shellCommand(new DocumentationAction() { + + @Override + public void perform() throws Exception { + AsciidoctorWriterTests.this.documentationWriter.println("foo"); + } + }); + + String expectedOutput = String.format("\n[source,bash]\n----\n$ foo\n----\n\n"); + assertEquals(expectedOutput, this.output.toString()); + } +} diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-different-rels.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json rename to spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-different-rels.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-same-rels.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json rename to spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-same-rels.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json b/spring-restdocs/src/test/resources/link-payloads/atom/no-links.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json rename to spring-restdocs/src/test/resources/link-payloads/atom/no-links.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs/src/test/resources/link-payloads/atom/single-link.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json rename to spring-restdocs/src/test/resources/link-payloads/atom/single-link.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs/src/test/resources/link-payloads/atom/wrong-format.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json rename to spring-restdocs/src/test/resources/link-payloads/atom/wrong-format.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-different-rels.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json rename to spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-different-rels.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-same-rels.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json rename to spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-same-rels.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json b/spring-restdocs/src/test/resources/link-payloads/hal/no-links.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json rename to spring-restdocs/src/test/resources/link-payloads/hal/no-links.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs/src/test/resources/link-payloads/hal/single-link.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json rename to spring-restdocs/src/test/resources/link-payloads/hal/single-link.json diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json b/spring-restdocs/src/test/resources/link-payloads/hal/wrong-format.json similarity index 100% rename from spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json rename to spring-restdocs/src/test/resources/link-payloads/hal/wrong-format.json From b60888cdc3315703a06a3df5f59fe418ef4f8ce7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 16:51:29 +0000 Subject: [PATCH 0014/1059] Add CONTRIBUTING.md --- CONTRIBUTING.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b7b7c8d23 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to Spring REST Docs + +Spring REST docs is released under the Apache 2.0 license. If you would like to +contribute something, or simply want to work with the code, this document should help you +to get started. + +## Sign the contributor license agreement + +Before we accept a non-trivial patch or pull request we will need you to sign the +[contributor's license agreement (CLA)][1]. Signing the contributor's agreement does not +grant anyone commit rights to the main repository, but it does mean that we can accept +your contributions, and you will get an author credit if we do. Please use "Andy +Wilkinson" in the project lead field when you complete the form. + + +## Code conventions and housekeeping + +None of these is essential for a pull request, but they will all help + +- Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +- Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +- Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +- Add some Javadocs +- Add unit tests that covers and new or modified functionality +- Whenever possible, please rebase your branch against the current master (or other + target branch in the main project). +* When writing a commit message please follow [these conventions][2]. Also, if you are + fixing an existing issue please add `Fixes gh-nnn` at the end of the commit message + (where nnn is the issue number). + +## Working with the code + +### Building from source + +To build the source you will need to use Java 8 – the main project only requires Java 7 +but the sample projects use Java 8. + +The code can be built with Gradle: + +``` +$ ./gradlew build +``` + +### Importing into Eclipse + +The project has Gradle's Eclipse plugin applied. Eclipse project and classpath metadata +can be generated by running the `eclipse` task: + +``` +$ ./gradlew eclipse +``` + +The project can then be imported into Eclipse using `File -> Import…` and then selecting +`General -> Existing Projects into Workspace`. + +[1]: https://support.springsource.com/spring_committer_signup +[2]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file From 326abdaf796033b2742c38de5d914dfc30f745a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 16:56:48 +0000 Subject: [PATCH 0015/1059] Update README to reflect new location of the samples --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ac61f8a64..1a1784753 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ To see the sample project's documentation, move into its directory and use Gradl build it. For example: ``` -$ cd rest-notes-spring-data-rest +$ cd samples/rest-notes-spring-data-rest $ ./gradlew asciidoctor ``` @@ -48,7 +48,7 @@ To see the sample project's documentation, move into its directory and use Maven it. For example: ``` -$ cd rest-notes-spring-hateoas +$ cd samples/rest-notes-spring-hateoas $ mvn package ``` @@ -333,8 +333,8 @@ To learn more, take a look at the accompanying sample projects: [4]: http://swagger.io [5]: http://plugins.gradle.org/plugin/org.asciidoctor.gradle.asciidoctor [6]: http://www.methods.co.nz/asciidoc/userguide.html#_system_macros -[7]: rest-notes-spring-data-rest -[8]: rest-notes-spring-hateoas +[7]: samples/rest-notes-spring-data-rest +[8]: samples/rest-notes-spring-hateoas [9]: https://speakerdeck.com/ankinson/documenting-restful-apis [10]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) [11]: https://build.spring.io/browse/SRD-PUB From cbae38d05de07d3b4928457dc00f783d8c2d0f21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 17:00:45 +0000 Subject: [PATCH 0016/1059] Update README with a link to video recording of the presentation --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a1784753..a6887fe8f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ with the [Spring MVC Test][2] framework. The result is intended to be an easy-to user guide, akin to [GitHub's API documentation][3] for example, rather than the fully automated, dense API documentation produced by tools like [Swagger][4]. -For a broader introduction see the [Documenting RESTful APIs][9] presentation. +For a broader introduction see the Documenting RESTful APIs presentation. Both the +[slides][9] and a [video recording][13] are available. ## Quickstart @@ -335,7 +336,8 @@ To learn more, take a look at the accompanying sample projects: [6]: http://www.methods.co.nz/asciidoc/userguide.html#_system_macros [7]: samples/rest-notes-spring-data-rest [8]: samples/rest-notes-spring-hateoas -[9]: https://speakerdeck.com/ankinson/documenting-restful-apis +[9]: https://speakerdeck.com/ankinson/documenting-restful-apis-webinar [10]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) [11]: https://build.spring.io/browse/SRD-PUB [12]: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-static-content +[13]: https://www.youtube.com/watch?v=knH5ihPNiUs&feature=youtu.be From e464127278927d0fbaadac48344cdf8186b14583 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 17:52:37 +0000 Subject: [PATCH 0017/1059] Add Sonar and Jacoco-based test coverage to the build --- build.gradle | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/build.gradle b/build.gradle index 3337251d9..e112b933f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ project(':spring-restdocs') { ext { jacksonVersion = '2.3.4' + jacocoVersion = '0.7.2.201409121644' junitVersion = '4.11' servletApiVersion = '3.1.0' springVersion = '4.1.4.RELEASE' @@ -12,6 +13,22 @@ project(':spring-restdocs') { apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'maven' + apply plugin: 'sonar-runner' + + sonarRunner { + sonarProperties { + property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" + property 'sonar.java.coveragePlugin', 'jacoco' + property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' + property 'sonar.links.homepage', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.issue', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.scm', 'https://github.com/spring-projects/spring-restdocs' + } + } + + configurations { + jacoco + } sourceCompatibility = 1.7 targetCompatibility = 1.7 @@ -44,6 +61,11 @@ project(':spring-restdocs') { compile "org.springframework:spring-web:$springVersion" compile "javax.servlet:javax.servlet-api:$servletApiVersion" compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" + } + + test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" } } From 81c6e8806094fbe33f8e99001cdef04c15cc3f22 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 19:53:47 +0000 Subject: [PATCH 0018/1059] Declare IterableEnumeration final as it only has private constructors --- .../org/springframework/restdocs/util/IterableEnumeration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java index 0dc04bb11..a1d256ddf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/IterableEnumeration.java @@ -26,7 +26,7 @@ * * @param the type of the Enumeration's contents */ -public class IterableEnumeration implements Iterable { +public final class IterableEnumeration implements Iterable { private final Enumeration enumeration; From d9183044916b7f96cff0ebbc5dfed4002a087ddf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Feb 2015 20:12:31 +0000 Subject: [PATCH 0019/1059] Polishing --- .../restdocs/curl/CurlDocumentation.java | 10 +++++----- .../restdocs/curl/CurlSnippetResultHandler.java | 11 +++++++++-- .../restdocs/hypermedia/LinkExtractors.java | 5 ++--- .../hypermedia/LinkSnippetResultHandler.java | 4 +++- .../restdocs/snippet/AsciidoctorWriter.java | 7 ++++--- .../restdocs/snippet/DocumentationWriter.java | 13 +++++++------ .../snippet/SnippetWritingResultHandler.java | 9 +++++++-- .../restdocs/snippet/AsciidoctorWriterTests.java | 7 +++---- 8 files changed, 40 insertions(+), 26 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 820058760..695448ae4 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -52,7 +52,7 @@ public static CurlSnippetResultHandler documentCurlRequest(String outputDir) { @Override public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { + throws IOException { writer.shellCommand(new CurlRequestDocumentationAction(writer, result, getCurlConfiguration())); } @@ -71,7 +71,7 @@ public static CurlSnippetResultHandler documentCurlResponse(String outputDir) { @Override public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { + throws IOException { writer.codeBlock("http", new CurlResponseDocumentationAction(writer, result, getCurlConfiguration())); } @@ -90,7 +90,7 @@ public static CurlSnippetResultHandler documentCurlRequestAndResponse(String out @Override public void handle(MvcResult result, DocumentationWriter writer) - throws Exception { + throws IOException { writer.shellCommand(new CurlRequestDocumentationAction(writer, result, getCurlConfiguration())); writer.codeBlock("http", new CurlResponseDocumentationAction(writer, @@ -116,7 +116,7 @@ private static final class CurlRequestDocumentationAction implements } @Override - public void perform() throws Exception { + public void perform() throws IOException { MockHttpServletRequest request = this.result.getRequest(); this.writer.print(String.format("curl %s://%s:%d%s", request.getScheme(), request.getRemoteHost(), request.getRemotePort(), @@ -169,7 +169,7 @@ private static final class CurlResponseDocumentationAction implements } @Override - public void perform() throws Exception { + public void perform() throws IOException { if (this.curlConfiguration.isIncludeResponseHeaders()) { HttpStatus status = HttpStatus.valueOf(this.result.getResponse() .getStatus()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java index 31a687cf7..b4ba9bc43 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java @@ -37,8 +37,15 @@ CurlConfiguration getCurlConfiguration() { return this.curlConfiguration; } - public CurlSnippetResultHandler includeResponseHeaders() { - this.curlConfiguration.setIncludeResponseHeaders(true); + /** + * Specify whether or not the generated cURL snippets should have contents as if cURL + * had been invoked with {@code -i, --include}. + * + * @param include {@code true} to use {@code -i, --include}, otherwise false + * @return {@code this} + */ + public CurlSnippetResultHandler includeResponseHeaders(boolean include) { + this.curlConfiguration.setIncludeResponseHeaders(include); return this; } } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java index 07ffd203c..4b1551aea 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java @@ -95,9 +95,9 @@ public Map> extractLinks(MockHttpServletResponse response) protected abstract Map> extractLinks(Map json); } + @SuppressWarnings("unchecked") private static class HalLinkExtractor extends JsonContentLinkExtractor { - @SuppressWarnings("unchecked") @Override public Map> extractLinks(Map json) { Map> extractedLinks = new HashMap<>(); @@ -112,7 +112,6 @@ public Map> extractLinks(Map json) { return extractedLinks; } - @SuppressWarnings("unchecked") private static List convertToLinks(Object object, String rel) { List links = new ArrayList<>(); if (object instanceof Collection) { @@ -141,9 +140,9 @@ private static void maybeAddLink(Link possibleLink, List links) { } } + @SuppressWarnings("unchecked") private static class AtomLinkExtractor extends JsonContentLinkExtractor { - @SuppressWarnings("unchecked") @Override public Map> extractLinks(Map json) { Map> extractedLinks = new HashMap<>(); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index ee8fc828c..bc4441c1f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -18,6 +18,7 @@ import static org.junit.Assert.fail; +import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -54,7 +55,8 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } @Override - protected void handle(MvcResult result, DocumentationWriter writer) throws Exception { + protected void handle(MvcResult result, DocumentationWriter writer) + throws IOException { Map> links; if (this.extractor != null) { links = this.extractor.extractLinks(result.getResponse()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java index 5791694b2..bd64c055c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.snippet; +import java.io.IOException; import java.io.Writer; /** @@ -35,11 +36,11 @@ public AsciidoctorWriter(Writer writer) { } @Override - public void shellCommand(final DocumentationAction action) throws Exception { + public void shellCommand(final DocumentationAction action) throws IOException { codeBlock("bash", new DocumentationAction() { @Override - public void perform() throws Exception { + public void perform() throws IOException { AsciidoctorWriter.this.print("$ "); action.perform(); } @@ -47,7 +48,7 @@ public void perform() throws Exception { } @Override - public void codeBlock(String language, DocumentationAction action) throws Exception { + public void codeBlock(String language, DocumentationAction action) throws IOException { println(); if (language != null) { println("[source," + language + "]"); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java index 83d0ee01f..054809981 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.snippet; +import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; @@ -37,9 +38,9 @@ protected DocumentationWriter(Writer writer) { * called the action, any necessary suffix is then written. * * @param action the action that will produce the shell command - * @throws Exception if the documentation fails + * @throws IOException if the documentation fails */ - public abstract void shellCommand(DocumentationAction action) throws Exception; + public abstract void shellCommand(DocumentationAction action) throws IOException; /** * Calls the given {@code action} to document a code block. The code block will be @@ -49,10 +50,10 @@ protected DocumentationWriter(Writer writer) { * * @param language the language in which the code is written * @param action the action that will produce the code - * @throws Exception if the documentation fails + * @throws IOException if the documentation fails */ public abstract void codeBlock(String language, DocumentationAction action) - throws Exception; + throws IOException; /** * Encapsulates an action that outputs some documentation. Typically implemented as a @@ -67,8 +68,8 @@ public interface DocumentationAction { /** * Perform the encapsulated action * - * @throws Exception if the action fails + * @throws IOException if the action fails */ - void perform() throws Exception; + void perform() throws IOException; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 5785cc8d9..f2e94caf5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -42,10 +42,10 @@ protected SnippetWritingResultHandler(String outputDir, String fileName) { } protected abstract void handle(MvcResult result, DocumentationWriter writer) - throws Exception; + throws IOException; @Override - public void handle(MvcResult result) throws Exception { + public void handle(MvcResult result) throws IOException { Writer writer = createWriter(); try { handle(result, new AsciidoctorWriter(writer)); @@ -62,6 +62,11 @@ private Writer createWriter() throws IOException { } if (outputFile != null) { + File parent = outputFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IllegalStateException("Failed to create directory '" + parent + + "'"); + } outputFile.getParentFile().mkdirs(); return new FileWriter(outputFile); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java index 33831d0b6..4151bfd8e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java @@ -18,12 +18,11 @@ import static org.junit.Assert.assertEquals; +import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import org.junit.Test; -import org.springframework.restdocs.snippet.AsciidoctorWriter; -import org.springframework.restdocs.snippet.DocumentationWriter; import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; /** @@ -42,7 +41,7 @@ public void codeBlock() throws Exception { this.documentationWriter.codeBlock("java", new DocumentationAction() { @Override - public void perform() throws Exception { + public void perform() throws IOException { AsciidoctorWriterTests.this.documentationWriter.println("foo"); } }); @@ -56,7 +55,7 @@ public void shellCommand() throws Exception { this.documentationWriter.shellCommand(new DocumentationAction() { @Override - public void perform() throws Exception { + public void perform() throws IOException { AsciidoctorWriterTests.this.documentationWriter.println("foo"); } }); From 3311121f3377d58071a63f8fd1caab6cc4239dea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Feb 2015 10:16:40 +0000 Subject: [PATCH 0020/1059] Polishing - move test classes into the appropriate packages --- .../restdocs/{core => }/RestDocumentationConfigurerTests.java | 2 +- .../restdocs/{core => hypermedia}/LinkExtractorsTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename spring-restdocs/src/test/java/org/springframework/restdocs/{core => }/RestDocumentationConfigurerTests.java (98%) rename spring-restdocs/src/test/java/org/springframework/restdocs/{core => hypermedia}/LinkExtractorsTests.java (98%) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java similarity index 98% rename from spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java index 5ff872ba7..0b8d7e560 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/core/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs; import static org.junit.Assert.assertEquals; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java similarity index 98% rename from spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java index f4a77439e..8af0ba5b4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/core/LinkExtractorsTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.core; +package org.springframework.restdocs.hypermedia; import static org.junit.Assert.assertEquals; From e8243457d0d60cf9534a4ca62725c80a0888f987 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Feb 2015 13:11:07 +0000 Subject: [PATCH 0021/1059] Improve testing of CurlDocumentation --- build.gradle | 1 + .../restdocs/StubMvcResult.java | 88 ++++++++ .../restdocs/curl/CurlDocumentationTests.java | 201 ++++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java diff --git a/build.gradle b/build.gradle index e112b933f..18ac9bd37 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ project(':spring-restdocs') { compile "javax.servlet:javax.servlet-api:$servletApiVersion" compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" + testCompile "org.springframework:spring-webmvc:$springVersion" } test { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java new file mode 100644 index 000000000..1afea51e8 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.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; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.servlet.FlashMap; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * A minimal stub implementation of {@link MvcResult} + * + * @author Andy Wilkinson + * + */ +public class StubMvcResult implements MvcResult { + + private final MockHttpServletRequest request; + + private final MockHttpServletResponse response; + + public StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { + this.request = request; + this.response = response; + } + + @Override + public MockHttpServletRequest getRequest() { + return this.request; + } + + @Override + public MockHttpServletResponse getResponse() { + return this.response; + } + + @Override + public Object getHandler() { + return null; + } + + @Override + public HandlerInterceptor[] getInterceptors() { + return null; + } + + @Override + public ModelAndView getModelAndView() { + return null; + } + + @Override + public Exception getResolvedException() { + return null; + } + + @Override + public FlashMap getFlashMap() { + return null; + } + + @Override + public Object getAsyncResult() { + return null; + } + + @Override + public Object getAsyncResult(long timeToWait) { + return null; + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java new file mode 100644 index 000000000..b68f8d504 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -0,0 +1,201 @@ +/* + * 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.curl; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; +import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; +import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.StubMvcResult; + +/** + * Tests for {@link CurlDocumentation} + * + * @author Andy Wilkinson + */ +public class CurlDocumentationTests { + + private final File outputDir = new File("build/curl-documentation-tests"); + + @Before + public void setup() { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + } + + @After + public void cleanup() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void getRequest() throws IOException { + documentCurlRequest("get-request").handle( + new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); + assertThat(requestSnippetLines("get-request"), + hasItem("$ curl http://localhost:80/foo -i")); + } + + @Test + public void nonGetRequest() throws IOException { + documentCurlRequest("non-get-request").handle( + new StubMvcResult(new MockHttpServletRequest("POST", "/foo"), null)); + assertThat(requestSnippetLines("non-get-request"), + hasItem("$ curl http://localhost:80/foo -i -X POST")); + } + + @Test + public void requestWithoutResponseHeaderInclusion() throws IOException { + documentCurlRequest("request-without-response-header-inclusion") + .includeResponseHeaders(false) + .handle(new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); + assertThat(requestSnippetLines("request-without-response-header-inclusion"), + hasItem("$ curl http://localhost:80/foo")); + } + + @Test + public void requestWithContent() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setContent("content".getBytes()); + documentCurlRequest("request-with-content").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-content"), + hasItem("$ curl http://localhost:80/foo -i -d 'content'")); + } + + @Test + public void requestWithHeaders() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setContentType(MediaType.APPLICATION_JSON_VALUE); + request.addHeader("a", "alpha"); + documentCurlRequest("request-with-headers").handle( + new StubMvcResult(request, null)); + assertThat( + requestSnippetLines("request-with-headers"), + hasItem("$ curl http://localhost:80/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); + } + + @Test + public void basicResponse() throws IOException { + documentCurlResponse("basic-response").handle( + new StubMvcResult(null, new MockHttpServletResponse())); + assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); + } + + @Test + public void nonOkResponse() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItem("HTTP/1.1 400 Bad Request")); + } + + @Test + public void responseWithHeaders() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setHeader("a", "alpha"); + documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); + } + + @Test + public void responseWithHeaderInclusionDisabled() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setHeader("a", "alpha"); + response.getWriter().append("content"); + documentCurlResponse("response-with-header-inclusion-disabled") + .includeResponseHeaders(false).handle(new StubMvcResult(null, response)); + List responseSnippetLines = responseSnippetLines("response-with-header-inclusion-disabled"); + assertThat( + responseSnippetLines, + not(hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", + "a: alpha"))); + assertThat(responseSnippetLines, hasItem("content")); + } + + @Test + public void responseWithContent() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("content"); + documentCurlResponse("response-with-content").handle( + new StubMvcResult(null, response)); + assertThat(responseSnippetLines("response-with-content"), + hasItems("HTTP/1.1 200 OK", "content")); + } + + @Test + public void requestAndResponse() throws IOException { + documentCurlRequestAndResponse("request-and-response").handle( + new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), + new MockHttpServletResponse())); + assertThat(requestResponseSnippetLines("request-and-response"), + hasItems("$ curl http://localhost:80/foo -i", "HTTP/1.1 200 OK")); + } + + private List requestSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "request"); + } + + private List responseSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "response"); + } + + private List requestResponseSnippetLines(String snippetName) + throws IOException { + return snippetLines(snippetName, "request-response"); + } + + private List snippetLines(String snippetName, String snippetType) + throws IOException { + File snippetDir = new File(this.outputDir, snippetName); + File snippetFile = new File(snippetDir, snippetType + ".asciidoc"); + String line = null; + List lines = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); + try { + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + finally { + reader.close(); + } + return lines; + } +} From 577e59de8f63d4b9fee3a47cad27523bbc0b8d57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Feb 2015 13:33:24 +0000 Subject: [PATCH 0022/1059] Polishing: remove duplicate call to mkdirs() --- .../restdocs/snippet/SnippetWritingResultHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index f2e94caf5..4ae21075f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -67,7 +67,6 @@ private Writer createWriter() throws IOException { throw new IllegalStateException("Failed to create directory '" + parent + "'"); } - outputFile.getParentFile().mkdirs(); return new FileWriter(outputFile); } From 35d4bc4d4fa43d01f66b38e7b32fc6411c9b5429 Mon Sep 17 00:00:00 2001 From: Andreas Evers Date: Sun, 22 Feb 2015 17:46:23 +0100 Subject: [PATCH 0023/1059] Update README after rename of spring-restdocs-core to spring-restdocs Closes gh-22 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a6887fe8f..8c80cb5a4 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ plugins { } ``` -Add a dependency on `spring-restdocs-core` in the `testCompile` configuration: +Add a dependency on `spring-restdocs` in the `testCompile` configuration: ```groovy dependencies { - testCompile 'org.springframework.restdocs:spring-restdocs-core:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' } ``` @@ -138,12 +138,12 @@ jar { You can look at either samples' `pom.xml` file to see the required configuration. The key parts are described below: -Add a dependency on `spring-restdocs-core` in the `test` scope: +Add a dependency on `spring-restdocs` in the `test` scope: ```xml org.springframework.restdocs - spring-restdocs-core + spring-restdocs 0.1.0.BUILD-SNAPSHOT test From 6bdd60cda6760184b66272502d70c8023febfc2a Mon Sep 17 00:00:00 2001 From: andrey Date: Sat, 21 Feb 2015 17:28:10 +0300 Subject: [PATCH 0024/1059] Include query string in curl request snippets --- .../restdocs/curl/CurlDocumentation.java | 7 ++++++- .../restdocs/curl/CurlDocumentationTests.java | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 695448ae4..7c91e4d0c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -120,7 +120,7 @@ public void perform() throws IOException { MockHttpServletRequest request = this.result.getRequest(); this.writer.print(String.format("curl %s://%s:%d%s", request.getScheme(), request.getRemoteHost(), request.getRemotePort(), - request.getRequestURI())); + getRequestUriWithQueryString(request))); if (this.curlConfiguration.isIncludeResponseHeaders()) { this.writer.print(" -i"); @@ -145,6 +145,11 @@ public void perform() throws IOException { this.writer.println(); } + private String getRequestUriWithQueryString(MockHttpServletRequest request) { + return request.getQueryString() != null ? request.getRequestURI() + "?" + + request.getQueryString() : request.getRequestURI(); + } + private String getContent(MockHttpServletRequest request) throws IOException { StringWriter bodyWriter = new StringWriter(); FileCopyUtils.copy(request.getReader(), bodyWriter); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index b68f8d504..95011d115 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -95,6 +95,25 @@ public void requestWithContent() throws IOException { hasItem("$ curl http://localhost:80/foo -i -d 'content'")); } + @Test + public void requestWitUriQueryString() throws IOException { + documentCurlRequest("request-with-uri-query-string").handle( + new StubMvcResult(new MockHttpServletRequest("GET", "/foo?param=value"), + null)); + assertThat(requestSnippetLines("request-with-uri-query-string"), + hasItem("$ curl http://localhost:80/foo?param=value -i")); + } + + @Test + public void requestWithQueryString() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setQueryString("param=value"); + documentCurlRequest("request-with-query-string").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-query-string"), + hasItem("$ curl http://localhost:80/foo?param=value -i")); + } + @Test public void requestWithHeaders() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); From c41542f4649bd7bba0068ba6123a7b4a185e192d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Feb 2015 14:25:26 +0000 Subject: [PATCH 0025/1059] =?UTF-8?q?Omit=20port=20in=20curl=20request=20s?= =?UTF-8?q?nippet=20if=20it=E2=80=99s=20the=20default=20for=20the=20scheme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously an HTTP request to localhost on port 80 or an HTTPS request to localhost on 443 would generate curl request snippets with the uris http://localhost:80 and https://localhost:443 respectively. While specifying the port does no harm, it is unnecessary. This commit updates CurlDocumentation so that the uri in the request snippet does not include the port when the port is the default for the uri’s scheme. Closes gh-17 --- .../restdocs/curl/CurlDocumentation.java | 29 +++++++++-- .../restdocs/curl/CurlDocumentationTests.java | 48 +++++++++++++++---- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 7c91e4d0c..bb9a3ee27 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -21,6 +21,8 @@ import java.io.IOException; import java.io.StringWriter; +import javax.servlet.http.HttpServletRequest; + import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.snippet.DocumentationWriter; @@ -102,6 +104,14 @@ public void handle(MvcResult result, DocumentationWriter writer) private static final class CurlRequestDocumentationAction implements DocumentationAction { + private static final String SCHEME_HTTP = "http"; + + private static final String SCHEME_HTTPS = "https"; + + private static final int STANDARD_PORT_HTTP = 80; + + private static final int STANDARD_PORT_HTTPS = 443; + private final DocumentationWriter writer; private final MvcResult result; @@ -118,9 +128,14 @@ private static final class CurlRequestDocumentationAction implements @Override public void perform() throws IOException { MockHttpServletRequest request = this.result.getRequest(); - this.writer.print(String.format("curl %s://%s:%d%s", request.getScheme(), - request.getRemoteHost(), request.getRemotePort(), - getRequestUriWithQueryString(request))); + this.writer.print(String.format("curl %s://%s", request.getScheme(), + request.getRemoteHost())); + + if (isNonStandardPort(request)) { + this.writer.print(String.format(":%d", request.getRemotePort())); + } + + this.writer.print(getRequestUriWithQueryString(request)); if (this.curlConfiguration.isIncludeResponseHeaders()) { this.writer.print(" -i"); @@ -145,7 +160,13 @@ public void perform() throws IOException { this.writer.println(); } - private String getRequestUriWithQueryString(MockHttpServletRequest request) { + private boolean isNonStandardPort(HttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getRemotePort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request + .getRemotePort() != STANDARD_PORT_HTTPS); + } + + private String getRequestUriWithQueryString(HttpServletRequest request) { return request.getQueryString() != null ? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI(); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 95011d115..8b590ec52 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -65,7 +65,7 @@ public void getRequest() throws IOException { documentCurlRequest("get-request").handle( new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); assertThat(requestSnippetLines("get-request"), - hasItem("$ curl http://localhost:80/foo -i")); + hasItem("$ curl http://localhost/foo -i")); } @Test @@ -73,7 +73,7 @@ public void nonGetRequest() throws IOException { documentCurlRequest("non-get-request").handle( new StubMvcResult(new MockHttpServletRequest("POST", "/foo"), null)); assertThat(requestSnippetLines("non-get-request"), - hasItem("$ curl http://localhost:80/foo -i -X POST")); + hasItem("$ curl http://localhost/foo -i -X POST")); } @Test @@ -82,7 +82,7 @@ public void requestWithoutResponseHeaderInclusion() throws IOException { .includeResponseHeaders(false) .handle(new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); assertThat(requestSnippetLines("request-without-response-header-inclusion"), - hasItem("$ curl http://localhost:80/foo")); + hasItem("$ curl http://localhost/foo")); } @Test @@ -92,7 +92,7 @@ public void requestWithContent() throws IOException { documentCurlRequest("request-with-content").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-content"), - hasItem("$ curl http://localhost:80/foo -i -d 'content'")); + hasItem("$ curl http://localhost/foo -i -d 'content'")); } @Test @@ -101,7 +101,7 @@ public void requestWitUriQueryString() throws IOException { new StubMvcResult(new MockHttpServletRequest("GET", "/foo?param=value"), null)); assertThat(requestSnippetLines("request-with-uri-query-string"), - hasItem("$ curl http://localhost:80/foo?param=value -i")); + hasItem("$ curl http://localhost/foo?param=value -i")); } @Test @@ -111,7 +111,7 @@ public void requestWithQueryString() throws IOException { documentCurlRequest("request-with-query-string").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-query-string"), - hasItem("$ curl http://localhost:80/foo?param=value -i")); + hasItem("$ curl http://localhost/foo?param=value -i")); } @Test @@ -123,7 +123,7 @@ public void requestWithHeaders() throws IOException { new StubMvcResult(request, null)); assertThat( requestSnippetLines("request-with-headers"), - hasItem("$ curl http://localhost:80/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); + hasItem("$ curl http://localhost/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); } @Test @@ -184,7 +184,39 @@ public void requestAndResponse() throws IOException { new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), new MockHttpServletResponse())); assertThat(requestResponseSnippetLines("request-and-response"), - hasItems("$ curl http://localhost:80/foo -i", "HTTP/1.1 200 OK")); + hasItems("$ curl http://localhost/foo -i", "HTTP/1.1 200 OK")); + } + + @Test + public void httpWithNonStandardPort() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setRemotePort(8080); + documentCurlRequest("http-with-non-standard-port").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("http-with-non-standard-port"), + hasItem("$ curl http://localhost:8080/foo -i")); + } + + @Test + public void httpsWithStandardPort() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setRemotePort(443); + request.setScheme("https"); + documentCurlRequest("https-with-standard-port").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("https-with-standard-port"), + hasItem("$ curl https://localhost/foo -i")); + } + + @Test + public void httpsWithNonStandardPort() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setRemotePort(8443); + request.setScheme("https"); + documentCurlRequest("https-with-non-standard-port").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("https-with-non-standard-port"), + hasItem("$ curl https://localhost:8443/foo -i")); } private List requestSnippetLines(String snippetName) throws IOException { From ea77bd530ae6e0c48cd54f663f98f1ad5caf0a57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 Feb 2015 11:30:22 +0000 Subject: [PATCH 0026/1059] Document Spring Framework 4.1 as a requirement Closes gh-20 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c80cb5a4..1fa0548ef 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ For a broader introduction see the Documenting RESTful APIs presentation. Both t ## Quickstart -The project requires Java 7 or later. Snapshots are published to +The project requires Spring Framework 4.1 and Java 7 or later. Snapshots are published to `https://repo.spring.io/snapshot`. Alternatively, it can be built locally using Gradle: ``` From 565a57e5da2c5a0666259ab74abfb22d74c72537 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 Feb 2015 11:32:22 +0000 Subject: [PATCH 0027/1059] Update the README following rename of RestDocumentationConfiguration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fa0548ef..f0e5bacde 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ documentation snippets: public void setUp() { this.mockMvc = MockMvcBuilders .webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration() + .apply(new RestDocumentationConfigurer() .withScheme("https") .withHost("localhost") .withPort(8443)) From 289ba5715982b0cb4245bf74fe7f251853e29450 Mon Sep 17 00:00:00 2001 From: Ronald Kurr Date: Wed, 18 Feb 2015 10:38:15 -0500 Subject: [PATCH 0028/1059] Update README to recommend that pretty printing of JSON is enabled Closes gh-16 --- README.md | 7 +++++++ .../src/main/resources/application.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f0e5bacde..e86e47bb4 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,13 @@ be written: - `index/response.asciidoc` - `index/request-response.asciidoc` +#### Pretty-printed snippets + +To improve the readability of the generated snippets you may want to configure your +application to produce JSON that has been pretty-printed. If you are a Spring Boot +user you can do so by setting the `spring.jackson.serialization.indent_output` property +to `true`. + ### Hand-written documentation Producing high-quality, easily readable documentation is difficult and the process is diff --git a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties b/samples/rest-notes-spring-hateoas/src/main/resources/application.properties index aa4637f18..8e06a8284 100644 --- a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties +++ b/samples/rest-notes-spring-hateoas/src/main/resources/application.properties @@ -1 +1 @@ -http.mappers.json-pretty-print: true \ No newline at end of file +spring.jackson.serialization.indent_output: true \ No newline at end of file From 1eeb60510828f1405ad784df7b58f3148b601704 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Feb 2015 10:56:01 +0000 Subject: [PATCH 0029/1059] Support using alwaysDo to automatically document every MockMvc call The MVC test framework provides an alwaysDo method which allows the configuration of a result handler to be that will be invoked for every call to perform. Previously, this method could not be used for producing documentation snippets as the snippets would overwrite each other. This commit adds support for writing snippets to a parameterized output directory. Two parameters are supported: method name and step. Method name is the name of the currently executing test method. Step is a count of the number of calls to MockMvc.perform that have been made in that method. Closes gh-14 --- README.md | 33 ++++ build.gradle | 2 + .../asciidoc/getting-started-guide.asciidoc | 42 ++--- .../src/main/resources/application.properties | 1 + .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 95 ++++------- .../asciidoc/getting-started-guide.asciidoc | 44 ++--- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 72 +++----- .../RestDocumentationConfigurer.java | 7 +- .../config/RestDocumentationContext.java | 88 ++++++++++ ...estDocumentationTestExecutionListener.java | 40 +++++ .../restdocs/snippet/OutputFileResolver.java | 101 ++++++++++++ .../snippet/SnippetWritingResultHandler.java | 18 +- .../main/resources/META-INF/spring.factories | 1 + .../RestDocumentationIntegrationTests.java | 141 ++++++++++++++++ .../RestDocumentationConfigurerTests.java | 4 +- .../snippet/OutputFileResolverTests.java | 155 ++++++++++++++++++ 18 files changed, 681 insertions(+), 167 deletions(-) create mode 100644 samples/rest-notes-spring-data-rest/src/main/resources/application.properties rename spring-restdocs/src/main/java/org/springframework/restdocs/{ => config}/RestDocumentationConfigurer.java (93%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java create mode 100644 spring-restdocs/src/main/resources/META-INF/spring.factories create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{ => config}/RestDocumentationConfigurerTests.java (95%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java diff --git a/README.md b/README.md index e86e47bb4..9bacefee6 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,39 @@ be written: - `index/response.asciidoc` - `index/request-response.asciidoc` +#### Parameterized output directories + +The `document` method supports parameterized output directories. The following parameters +are supported: + +| Parameter | Description +| ------------- | ----------- +| {methodName} | The name of the test method, formatted using camelcase +| {method-name} | The name of the test method, formatted with dash separators +| {method_name} | The name of the test method, formatted with underscore separators +| {step} | The count of calls to `MockMvc.perform` in the current test + +For example, `document("{method-name}")` in a test method named `creatingANote` will +write snippets into a directory named `creating-a-note`. + +The `{step}` parameter is particularly useful in combination with Spring MVC Test's +`alwaysDo` functionality. It allows documentation to be configured once in a setup method: + +```java +@Before +public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); +} +``` + +With this configuration in place, every call to `MockMvc.perform` will produce +documentation snippets without any further configuration. Take a look at the +`GettingStartedDocumentation` classes in each of the sample applications to see this +functionality in action. + #### Pretty-printed snippets To improve the readability of the generated snippets you may want to configure your diff --git a/build.gradle b/build.gradle index 18ac9bd37..380f55599 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ project(':spring-restdocs') { junitVersion = '4.11' servletApiVersion = '3.1.0' springVersion = '4.1.4.RELEASE' + mockitoVersion = '1.10.19' } group = 'org.springframework.restdocs' @@ -63,6 +64,7 @@ project(':spring-restdocs') { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" testCompile "org.springframework:spring-webmvc:$springVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" } test { diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc index 5f8e71db0..e7781f242 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/request.asciidoc[] +include::{generated}/index/1/request.asciidoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] To create a note, you need to execute a `POST` request to this URI including a JSON payload containing the title and body of the note: -include::{generated}/create-note/request.asciidoc[] +include::{generated}/creating-a-note/1/request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/create-note/response.asciidoc[] +include::{generated}/creating-a-note/1/response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example, you can access the note's details by performing a `GET` request: -include::{generated}/get-note/request.asciidoc[] +include::{generated}/creating-a-note/2/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/response.asciidoc[] Note the `tags` link which we'll make use of later. @@ -92,26 +92,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/create-tag/request.asciidoc[] +include::{generated}/creating-a-note/3/request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/create-tag/response.asciidoc[] +include::{generated}/creating-a-note/3/response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/get-tag/request.asciidoc[] +include::{generated}/creating-a-note/4/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/get-tag/response.asciidoc[] +include::{generated}/creating-a-note/4/response.asciidoc[] @@ -132,24 +132,24 @@ with it. Once again we execute a `POST` request. However, this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/create-tagged-note/request.asciidoc[] +include::{generated}/creating-a-note/5/request.asciidoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/create-tagged-note/response.asciidoc[] +include::{generated}/creating-a-note/5/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/get-tagged-note/request-response.asciidoc[] +include::{generated}/creating-a-note/6/request-response.asciidoc[] To verify that the tag has been associated with the note, we can perform a `GET` request against the URI from the `tags` link: -include::{generated}/get-tags/request.asciidoc[] +include::{generated}/creating-a-note/7/request.asciidoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/get-tags/response.asciidoc[] +include::{generated}/creating-a-note/7/response.asciidoc[] @@ -159,17 +159,17 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll used the URI of the untagged note that we created earlier: -include::{generated}/tag-existing-note/request.asciidoc[] +include::{generated}/creating-a-note/8/request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/tag-existing-note/response.asciidoc[] +include::{generated}/creating-a-note/8/response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/get-tags-for-existing-note/request-response.asciidoc[] +include::{generated}/creating-a-note/9/request-response.asciidoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/resources/application.properties b/samples/rest-notes-spring-data-rest/src/main/resources/application.properties new file mode 100644 index 000000000..8e06a8284 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.jackson.serialization.indent_output: true \ No newline at end of file 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 55fe34e85..e5902a612 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 @@ -39,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index ec54ff5d2..1657af9a1 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -38,7 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -66,33 +66,33 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } @Test public void index() throws Exception { this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("index")); + .andExpect(status().isOk()) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))); } @Test public void creatingANote() throws JsonProcessingException, Exception { String noteLocation = createNote(); - getNote(noteLocation); + MvcResult note = getNote(noteLocation); String tagLocation = createTag(); getTag(tagLocation); String taggedNoteLocation = createTaggedNote(tagLocation); - getTaggedNote(taggedNoteLocation); - getTags(taggedNoteLocation); + MvcResult taggedNote = getNote(taggedNoteLocation); + getTags(getLink(taggedNote, "tags")); tagExistingNote(noteLocation, tagLocation); - getTaggedExistingNote(noteLocation); - getTagsForExistingNote(noteLocation); + getTags(getLink(note, "tags")); } String createNote() throws Exception { @@ -106,18 +106,17 @@ String createNote() throws Exception { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getNote(String noteLocation) throws Exception { - this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("get-note")); + MvcResult getNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andReturn(); } String createTag() throws Exception, JsonProcessingException { @@ -128,19 +127,16 @@ String createTag() throws Exception, JsonProcessingException { .perform( post("/tags").contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tag")) - .andReturn().getResponse().getHeader("Location"); + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue())) + .andReturn().getResponse().getHeader("Location"); return tagLocation; } void getTag(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk()) + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andDo(document("get-tag")); + .andExpect(jsonPath("_links.notes", is(notNullValue()))); } String createTaggedNote(String tag) throws Exception { @@ -155,28 +151,14 @@ String createTaggedNote(String tag) throws Exception { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("get-tagged-note")); - } - - void getTags(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "tags"); - - this.mockMvc.perform(get(tagsLocation)) + void getTags(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { @@ -186,29 +168,24 @@ void tagExistingNote(String noteLocation, String tagLocation) throws Exception { this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()) - .andDo(document("tag-existing-note")); - + .andExpect(status().isNoContent()); } - void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + MvcResult getTaggedExistingNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) - .andDo(document("get-tagged-existing-note")); + .andReturn(); } - void getTagsForExistingNote(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)) + void getTagsForExistingNote(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags-for-existing-note")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } - private String getLink(MvcResult result, String href) + private String getLink(MvcResult result, String rel) throws UnsupportedEncodingException { return JsonPath.parse(result.getResponse().getContentAsString()).read( - "_links.tags.href"); + "_links." + rel + ".href"); } } diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc index ce970ea9e..8fccc65c6 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/request.asciidoc[] +include::{generated}/index/1/request.asciidoc[] This request should yield the following response: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] To create a note you need to execute a `POST` request to this URI, including a JSON payload containing the title and body of the note: -include::{generated}/create-note/request.asciidoc[] +include::{generated}/creating-a-note/1/request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/create-note/response.asciidoc[] +include::{generated}/creating-a-note/1/response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example you can access the note's details by performing a `GET` request: -include::{generated}/get-note/request.asciidoc[] +include::{generated}/creating-a-note/2/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/response.asciidoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/create-tag/request.asciidoc[] +include::{generated}/creating-a-note/3/request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/create-tag/response.asciidoc[] +include::{generated}/creating-a-note/3/response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/get-tag/request.asciidoc[] +include::{generated}/creating-a-note/4/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/get-tag/response.asciidoc[] +include::{generated}/creating-a-note/4/response.asciidoc[] @@ -131,44 +131,44 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/create-tagged-note/request.asciidoc[] +include::{generated}/creating-a-note/5/request.asciidoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/create-tagged-note/response.asciidoc[] +include::{generated}/creating-a-note/5/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/get-tagged-note/request-response.asciidoc[] +include::{generated}/creating-a-note/6/request-response.asciidoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/get-tags/request.asciidoc[] +include::{generated}/creating-a-note/7/request.asciidoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/get-tags/response.asciidoc[] +include::{generated}/creating-a-note/7/response.asciidoc[] [getting-started-tagging-a-note-existing] === Tagging an existing note An existing note can be tagged by executing a `PATCH` request against the note's URI with -a body that contains the array of tags to be associated with the note. We'll used the +a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/tag-existing-note/request.asciidoc[] +include::{generated}/creating-a-note/8/request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/tag-existing-note/response.asciidoc[] +include::{generated}/creating-a-note/8/response.asciidoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/get-tags-for-existing-note/request-response.asciidoc[] +include::{generated}/creating-a-note/9/request-response.asciidoc[] 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 d82451c84..bcb731bf0 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 @@ -39,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 49892b521..5b95a35a8 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -23,7 +23,6 @@ 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; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,7 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -67,7 +66,9 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } @Test @@ -75,25 +76,23 @@ public void index() throws Exception { this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("index")); + .andExpect(jsonPath("_links.tags", is(notNullValue()))); } @Test public void creatingANote() throws JsonProcessingException, Exception { String noteLocation = createNote(); - getNote(noteLocation); + MvcResult note = getNote(noteLocation); String tagLocation = createTag(); getTag(tagLocation); String taggedNoteLocation = createTaggedNote(tagLocation); - getTaggedNote(taggedNoteLocation); - getTags(taggedNoteLocation); + MvcResult taggedNote = getNote(taggedNoteLocation); + getTags(getLink(taggedNote, "note-tags")); tagExistingNote(noteLocation, tagLocation); - getTaggedExistingNote(noteLocation); - getTagsForExistingNote(noteLocation); + getTags(getLink(note, "note-tags")); } String createNote() throws Exception { @@ -107,17 +106,17 @@ String createNote() throws Exception { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getNote(String noteLocation) throws Exception { - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + MvcResult getNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) .andExpect(jsonPath("title", is(notNullValue()))) .andExpect(jsonPath("body", is(notNullValue()))) .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(document("get-note")); + .andReturn(); } String createTag() throws Exception, JsonProcessingException { @@ -130,7 +129,6 @@ String createTag() throws Exception, JsonProcessingException { objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tag")) .andReturn().getResponse().getHeader("Location"); return tagLocation; } @@ -138,8 +136,7 @@ String createTag() throws Exception, JsonProcessingException { void getTag(String tagLocation) throws Exception { this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))) - .andDo(document("get-tag")); + .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))); } String createTaggedNote(String tag) throws Exception { @@ -154,28 +151,14 @@ String createTaggedNote(String tag) throws Exception { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + void getTags(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(document("get-tagged-note")); - } - - void getTags(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "note-tags"); - this.mockMvc.perform(get(tagsLocation)) - .andExpect(status().isOk()) - .andDo(print()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { @@ -185,24 +168,19 @@ void tagExistingNote(String noteLocation, String tagLocation) throws Exception { this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()) - .andDo(document("tag-existing-note")); - + .andExpect(status().isNoContent()); } - void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + MvcResult getTaggedExistingNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) - .andDo(document("get-tagged-existing-note")); + .andReturn(); } - void getTagsForExistingNote(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "note-tags"); - this.mockMvc.perform(get(tagsLocation)) + void getTagsForExistingNote(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags-for-existing-note")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } private String getLink(MvcResult result, String rel) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java similarity index 93% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index f644cbbd3..44f805115 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.config; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -100,6 +100,11 @@ public RequestPostProcessor beforeMockMvcCreated( @Override public MockHttpServletRequest postProcessRequest( MockHttpServletRequest request) { + RestDocumentationContext currentContext = RestDocumentationContext + .currentContext(); + if (currentContext != null) { + currentContext.getAndIncrementStepCount(); + } request.setScheme(RestDocumentationConfigurer.this.scheme); request.setRemotePort(RestDocumentationConfigurer.this.port); request.setServerPort(RestDocumentationConfigurer.this.port); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java new file mode 100644 index 000000000..8d409952e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.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.config; + +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@code RestDocumentationContext} encapsulates the context in which the documentation of + * a RESTful API is being performed. + * + * @author Andy Wilkinson + */ +public class RestDocumentationContext { + + private static final ThreadLocal CONTEXTS = new InheritableThreadLocal(); + + private final AtomicInteger stepCount = new AtomicInteger(0); + + private final Method testMethod; + + private RestDocumentationContext() { + this(null); + } + + private RestDocumentationContext(Method testMethod) { + this.testMethod = testMethod; + } + + /** + * Returns the test {@link Method method} that is currently executing + * + * @return The test method + */ + public Method getTestMethod() { + return this.testMethod; + } + + /** + * Gets and then increments the current step count + * + * @return The step count prior to it being incremented + */ + int getAndIncrementStepCount() { + return this.stepCount.getAndIncrement(); + } + + /** + * Gets the current step count + * + * @return The current step count + */ + public int getStepCount() { + return this.stepCount.get(); + } + + static void establishContext(Method testMethod) { + CONTEXTS.set(new RestDocumentationContext(testMethod)); + } + + static void clearContext() { + CONTEXTS.set(null); + } + + /** + * Returns the current context, never {@code null}. + * + * @return The current context + */ + public static RestDocumentationContext currentContext() { + return CONTEXTS.get(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java new file mode 100644 index 000000000..b685699b6 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.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.config; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * A {@link TestExecutionListener} that sets up and tears down the Spring REST Docs + * context for each test method + * + * @author Andy Wilkinson + */ +public class RestDocumentationTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + RestDocumentationContext.establishContext(testContext.getTestMethod()); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + RestDocumentationContext.clearContext(); + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java new file mode 100644 index 000000000..86b9facec --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java @@ -0,0 +1,101 @@ +/* + * 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.snippet; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.restdocs.config.RestDocumentationContext; + +/** + * {@code OutputFileResolver} resolves an absolute output file based on the current + * configuration and context. + * + * @author Andy Wilkinson + */ +class OutputFileResolver { + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])"); + + File resolve(String outputDirectory, String fileName) { + Map replacements = createReplacements(); + String path = outputDirectory; + for (Entry replacement : replacements.entrySet()) { + while (path.contains(replacement.getKey())) { + if (replacement.getValue() == null) { + throw new IllegalStateException("No replacement is available for " + + replacement.getKey()); + } + else { + path = path.replace(replacement.getKey(), replacement.getValue()); + } + } + } + + File outputFile = new File(path, fileName); + if (!outputFile.isAbsolute()) { + outputFile = makeRelativeToConfiguredOutputDir(outputFile); + } + return outputFile; + } + + private Map createReplacements() { + RestDocumentationContext context = RestDocumentationContext.currentContext(); + + Map replacements = new HashMap(); + replacements.put("{methodName}", context == null ? null : context.getTestMethod() + .getName()); + replacements.put("{method-name}", context == null ? null + : camelCaseToDash(context.getTestMethod().getName())); + replacements.put("{method_name}", context == null ? null + : camelCaseToUnderscore(context.getTestMethod().getName())); + replacements.put("{step}", + context == null ? null : Integer.toString(context.getStepCount())); + + return replacements; + } + + private String camelCaseToDash(String string) { + return camelCaseToSeparator(string, "-"); + } + + private String camelCaseToUnderscore(String string) { + return camelCaseToSeparator(string, "_"); + } + + private String camelCaseToSeparator(String string, String separator) { + Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase()); + } + matcher.appendTail(result); + return result.toString(); + } + + private File makeRelativeToConfiguredOutputDir(File outputFile) { + File configuredOutputDir = new DocumentationProperties().getOutputDir(); + if (configuredOutputDir != null) { + return new File(configuredOutputDir, outputFile.getPath()); + } + return null; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 4ae21075f..5df711765 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -56,10 +56,8 @@ public void handle(MvcResult result) throws IOException { } private Writer createWriter() throws IOException { - File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); - if (!outputFile.isAbsolute()) { - outputFile = makeRelativeToConfiguredOutputDir(outputFile); - } + File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName + + ".asciidoc"); if (outputFile != null) { File parent = outputFile.getParentFile(); @@ -69,15 +67,9 @@ private Writer createWriter() throws IOException { } return new FileWriter(outputFile); } - - return new OutputStreamWriter(System.out); - } - - private File makeRelativeToConfiguredOutputDir(File outputFile) { - File configuredOutputDir = new DocumentationProperties().getOutputDir(); - if (configuredOutputDir != null) { - return new File(configuredOutputDir, outputFile.getPath()); + else { + return new OutputStreamWriter(System.out); } - return null; } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/META-INF/spring.factories b/spring-restdocs/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..64519d788 --- /dev/null +++ b/spring-restdocs/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.test.context.TestExecutionListener=org.springframework.restdocs.config.RestDocumentationTestExecutionListener \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java new file mode 100644 index 000000000..7c485b443 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -0,0 +1,141 @@ +package org.springframework.restdocs; + +import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationIntegrationTests.TestConfiguration; +import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Integration tests for Spring REST Docs + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = TestConfiguration.class) +public class RestDocumentationIntegrationTests { + + @Autowired + private WebApplicationContext context; + + @Before + public void setOutputDirSystemProperty() { + System.setProperty("org.springframework.restdocs.outputDir", + "build/generated-snippets"); + } + + @Before + public void deleteSnippets() { + FileSystemUtils.deleteRecursively(new File("build/generated-snippets")); + } + + @After + public void clearOutputDirSystemProperty() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void basicSnippetGeneration() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("basic")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), + "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + } + + @Test + public void parameterizedOutputDirectory() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("{method-name}")); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/parameterized-output-directory"), + "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + } + + @Test + public void multiStep() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}-{step}")).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-1/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-2/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-3/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + for (String snippet : snippets) { + assertTrue(new File(directory, snippet).isFile()); + } + } + + @Configuration + @EnableWebMvc + static class TestConfiguration extends WebMvcConfigurerAdapter { + + @Bean + public TestController testController() { + return new TestController(); + } + + } + + @RestController + static class TestController { + + @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public Map foo() { + Map response = new HashMap(); + response.put("a", "alpha"); + return response; + } + + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java similarity index 95% rename from spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0b8d7e560..a4a938288 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.config; import static org.junit.Assert.assertEquals; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; /** diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java new file mode 100644 index 000000000..757552849 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java @@ -0,0 +1,155 @@ +/* + * 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.snippet; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.restdocs.config.RestDocumentationTestExecutionListener; +import org.springframework.test.context.TestContext; + +/** + * Tests for {@link OutputFileResolver}. + * + * @author Andy Wilkinson + */ +public class OutputFileResolverTests { + + private final OutputFileResolver resolver = new OutputFileResolver(); + + @Test + public void noConfiguredOutputDirectoryAndRelativeInput() { + assertThat(this.resolver.resolve("foo", "bar.txt"), is(nullValue())); + } + + @Test + public void absoluteInput() { + String absolutePath = new File("foo").getAbsolutePath(); + assertThat(this.resolver.resolve(absolutePath, "bar.txt"), is(new File( + absolutePath, "bar.txt"))); + } + + @Test + public void configuredOutputAndRelativeInput() { + String outputDir = new File("foo").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolve("bar", "baz.txt"), is(new File(outputDir, + "bar/baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + + @Test + public void configuredOutputAndAbsoluteInput() { + String outputDir = new File("foo").getAbsolutePath(); + String absolutePath = new File("bar").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolve(absolutePath, "baz.txt"), is(new File( + absolutePath, "baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + + @Test(expected = IllegalStateException.class) + public void placeholderWithoutAReplacement() { + this.resolver.resolve("{method-name}", "foo.txt"); + } + + @Test + public void dashSeparatedMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("dashSeparatedMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{method-name}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("dash-separated-method-name").getAbsolutePath(), + "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void underscoreSeparatedMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("underscoreSeparatedMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat( + this.resolver.resolve(new File("{method_name}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("underscore_separated_method_name") + .getAbsolutePath(), "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void camelCaseMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("camelCaseMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{methodName}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("camelCaseMethodName").getAbsolutePath(), + "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void stepCount() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("stepCount"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{step}").getAbsolutePath(), + "foo.txt"), is(new File(new File("0").getAbsolutePath(), "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } +} From 7041c2b2029675fd07c37b6034deee0f0ef82a5d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Feb 2015 12:30:17 +0000 Subject: [PATCH 0030/1059] Polish: add missing copyright header --- .../RestDocumentationIntegrationTests.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 7c485b443..549b8fc94 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -1,3 +1,19 @@ +/* + * 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; import static org.junit.Assert.assertTrue; From d6bd4870fd1ac557acfe302eb020bca9f82ad19d Mon Sep 17 00:00:00 2001 From: Yann Le Guern Date: Fri, 6 Mar 2015 17:49:23 +0100 Subject: [PATCH 0031/1059] Fix AsciidoctorWriterTests on Windows Use String.format and %n so that the test's expectations are correct on Windows and Unix-like platforms. Fixes gh-29 --- .../restdocs/snippet/AsciidoctorWriterTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java index 4151bfd8e..9e56f3475 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java @@ -46,7 +46,7 @@ public void perform() throws IOException { } }); - String expectedOutput = String.format("\n[source,java]\n----\nfoo\n----\n\n"); + String expectedOutput = String.format("%n[source,java]%n----%nfoo%n----%n%n"); assertEquals(expectedOutput, this.output.toString()); } @@ -60,7 +60,7 @@ public void perform() throws IOException { } }); - String expectedOutput = String.format("\n[source,bash]\n----\n$ foo\n----\n\n"); + String expectedOutput = String.format("%n[source,bash]%n----%n$ foo%n----%n%n"); assertEquals(expectedOutput, this.output.toString()); } } From f649e1a17037c8cd8383acba463424d363567001 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2015 13:52:00 +0000 Subject: [PATCH 0032/1059] Add support to DocumentationWriter for producing tables This commit adds an API to DocumentationWriter for producing documentation snippets containing tables. The API is intended to be fairly simple such that it can be implemented for a variety of different output formats, and not just for Asciidoctor (the only currently supported format). Closes gh-34 --- .../restdocs/snippet/AsciidoctorWriter.java | 43 ++++++++++++++- .../restdocs/snippet/DocumentationWriter.java | 54 ++++++++++++++++++- .../snippet/AsciidoctorWriterTests.java | 20 +++++++ 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java index bd64c055c..fe35ef426 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java @@ -27,6 +27,12 @@ */ public class AsciidoctorWriter extends DocumentationWriter { + private static final String DELIMITER_CODE_BLOCK = "----"; + + private static final String DELIMITER_TABLE = "|==="; + + private final TableWriter tableWriter = new AsciidoctorTableWriter(); + /** * Creates a new {@code AsciidoctorWriter} that will write to the given {@code writer} * @param writer The writer to which output will be written @@ -53,10 +59,43 @@ public void codeBlock(String language, DocumentationAction action) throws IOExce if (language != null) { println("[source," + language + "]"); } - println("----"); + println(DELIMITER_CODE_BLOCK); action.perform(); - println("----"); + println(DELIMITER_CODE_BLOCK); println(); } + @Override + public void table(TableAction action) throws IOException { + println(); + println(DELIMITER_TABLE); + action.perform(this.tableWriter); + println(DELIMITER_TABLE); + println(); + } + + private final class AsciidoctorTableWriter implements TableWriter { + + @Override + public void headers(String... headers) { + StringBuilder builder = new StringBuilder(); + for (String header : headers) { + builder.append("|"); + builder.append(header); + } + println(builder.toString()); + println(); + } + + @Override + public void row(String... entries) { + for (String entry : entries) { + print("|"); + println(entry); + } + println(); + } + + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java index 054809981..b56344016 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java @@ -46,7 +46,8 @@ protected DocumentationWriter(Writer writer) { * Calls the given {@code action} to document a code block. The code block will be * annotated as containing code written in the given {@code language}. Any prefix * necessary for the documentation format is written prior to calling the - * {@code action}. Having called the action, any necessary suffix is the written. + * {@code action}. Having called the {@code action}, any necessary suffix is then + * written. * * @param language the language in which the code is written * @param action the action that will produce the code @@ -55,11 +56,20 @@ protected DocumentationWriter(Writer writer) { public abstract void codeBlock(String language, DocumentationAction action) throws IOException; + /** + * Calls the given {@code action} to document a table. Any prefix necessary for + * documenting a table is written prior to calling the {@code action}. Having called + * the {@code action}, any necessary suffix is then written. + * + * @param action the action that will produce the table + * @throws IOException if the documentation fails + */ + public abstract void table(TableAction action) throws IOException; + /** * Encapsulates an action that outputs some documentation. Typically implemented as a * lamda or, pre-Java 8, as an anonymous inner class. * - * @author Andy Wilkinson * @see DocumentationWriter#shellCommand * @see DocumentationWriter#codeBlock */ @@ -71,5 +81,45 @@ public interface DocumentationAction { * @throws IOException if the action fails */ void perform() throws IOException; + + } + + /** + * Encapsulates an action that outputs a table. + * + * @see DocumentationWriter#table(TableAction) + */ + public interface TableAction { + + /** + * Perform the encapsulated action + * + * @param tableWriter the writer to be used to write the table + * @throws IOException if the action fails + */ + void perform(TableWriter tableWriter) throws IOException; + + } + + /** + * A writer for producing a table + */ + public interface TableWriter { + + /** + * Writes the table's headers + * + * @param headers the headers + */ + void headers(String... headers); + + /** + * Writes a row in the table + * + * @param entries the entries in the row + */ + void row(String... entries); + } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java index 9e56f3475..15eb2728e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java @@ -24,6 +24,8 @@ import org.junit.Test; import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; +import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; /** * Tests for {@link AsciidoctorWriter} @@ -63,4 +65,22 @@ public void perform() throws IOException { String expectedOutput = String.format("%n[source,bash]%n----%n$ foo%n----%n%n"); assertEquals(expectedOutput, this.output.toString()); } + + @Test + public void table() throws Exception { + this.documentationWriter.table(new TableAction() { + + @Override + public void perform(TableWriter tableWriter) throws IOException { + tableWriter.headers("One", "Two", "Three"); + tableWriter.row("alpha", "bravo", "charlie"); + tableWriter.row("foo", "bar", "baz"); + } + + }); + String expectedOutput = String + .format("%n|===%n|One|Two|Three%n%n|alpha%n|bravo%n|charlie%n%n|foo%n|bar%n|baz%n%n|===%n%n"); + assertEquals(expectedOutput, this.output.toString()); + System.out.println(this.output.toString()); + } } From c3efc4128f7ba585004458fa20e4cf42b5f9a4d2 Mon Sep 17 00:00:00 2001 From: Yann Le Guern Date: Fri, 6 Mar 2015 18:56:42 +0100 Subject: [PATCH 0033/1059] Use request parameters to create query string for GET requests Previously, only the request's query string was considered when creating the curl command's request URI. This meant that query parameters configured via request.addParameter where not included in the URI. This commit enhances CurlRequestDocumentationAction so that, in the absence of a query string, it will use a GET request's parameters to produce the query string include in the curl command's URI. Closes gh-26 Closes gh-30 --- .../restdocs/curl/CurlDocumentation.java | 57 +++++++++++++++++-- .../restdocs/curl/CurlDocumentationTests.java | 33 +++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index bb9a3ee27..cd738df76 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -20,6 +20,9 @@ import java.io.IOException; import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; import javax.servlet.http.HttpServletRequest; @@ -29,6 +32,7 @@ import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; /** @@ -36,6 +40,7 @@ * the cURL command-line utility. * * @author Andy Wilkinson + * @author Yann Le Guern */ public abstract class CurlDocumentation { @@ -141,9 +146,8 @@ public void perform() throws IOException { this.writer.print(" -i"); } - RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod()); - if (requestMethod != RequestMethod.GET) { - this.writer.print(String.format(" -X %s", requestMethod.toString())); + if (!isGetRequest(request)) { + this.writer.print(String.format(" -X %s", request.getMethod())); } for (String headerName : iterable(request.getHeaderNames())) { @@ -160,6 +164,10 @@ public void perform() throws IOException { this.writer.println(); } + private boolean isGetRequest(HttpServletRequest request) { + return RequestMethod.GET == RequestMethod.valueOf(request.getMethod()); + } + private boolean isNonStandardPort(HttpServletRequest request) { return (SCHEME_HTTP.equals(request.getScheme()) && request.getRemotePort() != STANDARD_PORT_HTTP) || (SCHEME_HTTPS.equals(request.getScheme()) && request @@ -167,8 +175,47 @@ private boolean isNonStandardPort(HttpServletRequest request) { } private String getRequestUriWithQueryString(HttpServletRequest request) { - return request.getQueryString() != null ? request.getRequestURI() + "?" - + request.getQueryString() : request.getRequestURI(); + StringBuilder sb = new StringBuilder(); + sb.append(request.getRequestURI()); + String queryString = getQueryString(request); + if (StringUtils.hasText(queryString)) { + sb.append('?').append(queryString); + } + return sb.toString(); + } + + private String getQueryString(HttpServletRequest request) { + if (request.getQueryString() != null) { + return request.getQueryString(); + } + if (isGetRequest(request)) { + return toQueryString(request.getParameterMap()); + } + return null; + } + + private static String toQueryString(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + for (String value : entry.getValue()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(urlEncodeUTF8(entry.getKey())).append('=') + .append(urlEncodeUTF8(value)); + } + } + return sb.toString(); + } + + private static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + s + + " using UTF-8", ex); + } } private String getContent(MockHttpServletRequest request) throws IOException { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 8b590ec52..caaa90e65 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -44,6 +44,7 @@ * Tests for {@link CurlDocumentation} * * @author Andy Wilkinson + * @author Yann Le Guern */ public class CurlDocumentationTests { @@ -114,6 +115,38 @@ public void requestWithQueryString() throws IOException { hasItem("$ curl http://localhost/foo?param=value -i")); } + @Test + public void requestWithOneParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addParameter("k1", "v1"); + documentCurlRequest("request-with-one-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-one-parameter"), + hasItem("$ curl http://localhost/foo?k1=v1 -i")); + } + + @Test + public void requestWithMultipleParameters() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addParameter("k1", "v1"); + request.addParameter("k2", "v2"); + request.addParameter("k1", "v1-bis"); + documentCurlRequest("request-with-multiple-parameters").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-multiple-parameters"), + hasItem("$ curl http://localhost/foo?k1=v1&k1=v1-bis&k2=v2 -i")); + } + + @Test + public void requestWithUrlEncodedParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addParameter("k1", "foo bar&"); + documentCurlRequest("request-with-url-encoded-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-url-encoded-parameter"), + hasItem("$ curl http://localhost/foo?k1=foo+bar%26 -i")); + } + @Test public void requestWithHeaders() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); From 23a638817795a913396e86c4665ea4a8998a670e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Mar 2015 14:45:54 +0000 Subject: [PATCH 0034/1059] Escape & characaters in curl request URLs Without escaping, the & will cause bash to truncate the URL and background the curl process. Closes gh-35 --- .../org/springframework/restdocs/curl/CurlDocumentation.java | 4 ++-- .../springframework/restdocs/curl/CurlDocumentationTests.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index cd738df76..882806a6e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -186,7 +186,7 @@ private String getRequestUriWithQueryString(HttpServletRequest request) { private String getQueryString(HttpServletRequest request) { if (request.getQueryString() != null) { - return request.getQueryString(); + return request.getQueryString().replace("&", "\\&"); } if (isGetRequest(request)) { return toQueryString(request.getParameterMap()); @@ -199,7 +199,7 @@ private static String toQueryString(Map map) { for (Map.Entry entry : map.entrySet()) { for (String value : entry.getValue()) { if (sb.length() > 0) { - sb.append("&"); + sb.append("\\&"); } sb.append(urlEncodeUTF8(entry.getKey())).append('=') .append(urlEncodeUTF8(value)); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index caaa90e65..367350284 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -134,7 +134,7 @@ public void requestWithMultipleParameters() throws IOException { documentCurlRequest("request-with-multiple-parameters").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-multiple-parameters"), - hasItem("$ curl http://localhost/foo?k1=v1&k1=v1-bis&k2=v2 -i")); + hasItem("$ curl http://localhost/foo?k1=v1\\&k1=v1-bis\\&k2=v2 -i")); } @Test From 45fe87995f414711d96d6faeb3ddf9de4da810bd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Mar 2015 10:42:43 +0000 Subject: [PATCH 0035/1059] Use MAVEN_HOME when calling mvn to build the samples using Maven See gh-33 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 380f55599..fe72e388d 100644 --- a/build.gradle +++ b/build.gradle @@ -104,13 +104,13 @@ task buildRestNotesSpringDataRestSampleWithGradle(type: GradleBuild) { task buildRestNotesSpringHateoasSampleWithMaven(type: Exec) { dependsOn 'spring-restdocs:install' workingDir 'samples/rest-notes-spring-hateoas' - commandLine 'mvn', 'clean', 'package' + commandLine "${System.env.MAVEN_HOME}/bin/mvn", 'clean', 'package' } task buildRestNotesSpringDataRestSampleWithMaven(type: Exec) { dependsOn 'spring-restdocs:install' workingDir 'samples/rest-notes-spring-data-rest' - commandLine 'mvn', 'clean', 'package' + commandLine "${System.env.MAVEN_HOME}/bin/mvn", 'clean', 'package' } wrapper { From 8b133c7c66b2d2427c6b22ef6ba0d4823fe6260e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Mar 2015 14:06:33 +0000 Subject: [PATCH 0036/1059] Customize the mvn command based on the OS when building the samples See gh-33 --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fe72e388d..45429aa91 100644 --- a/build.gradle +++ b/build.gradle @@ -104,13 +104,15 @@ task buildRestNotesSpringDataRestSampleWithGradle(type: GradleBuild) { task buildRestNotesSpringHateoasSampleWithMaven(type: Exec) { dependsOn 'spring-restdocs:install' workingDir 'samples/rest-notes-spring-hateoas' - commandLine "${System.env.MAVEN_HOME}/bin/mvn", 'clean', 'package' + def suffix = File.separatorChar == '/' ? '' : '.bat' + commandLine "${System.env.MAVEN_HOME}/bin/mvn${suffix}", 'clean', 'package' } task buildRestNotesSpringDataRestSampleWithMaven(type: Exec) { dependsOn 'spring-restdocs:install' workingDir 'samples/rest-notes-spring-data-rest' - commandLine "${System.env.MAVEN_HOME}/bin/mvn", 'clean', 'package' + def suffix = File.separatorChar == '/' ? '' : '.bat' + commandLine "${System.env.MAVEN_HOME}/bin/mvn${suffix}", 'clean', 'package' } wrapper { From cc54e5d7b00a4d3db48602d39dd1cf8cd4b87ed1 Mon Sep 17 00:00:00 2001 From: Yann Le Guern Date: Wed, 18 Mar 2015 14:12:59 +0100 Subject: [PATCH 0037/1059] Consider parameters when performing a POST request Previously, when producing the curl snippet for a POST request, the request's parameter map was ignored. This commit updates the snippet generation to look at the parameter map and POST it to the server as application/x-www-form-urlencoded data using -d Closes gh-38 --- .../restdocs/curl/CurlDocumentation.java | 11 +++++++ .../restdocs/curl/CurlDocumentationTests.java | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 882806a6e..ee7f3cb99 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -160,6 +160,13 @@ public void perform() throws IOException { if (request.getContentLengthLong() > 0) { this.writer.print(String.format(" -d '%s'", getContent(request))); } + else if (isPostRequest(request)) { + Map parameters = request.getParameterMap(); + if (!parameters.isEmpty()) { + this.writer.print(String + .format(" -d '%s'", toQueryString(parameters))); + } + } this.writer.println(); } @@ -168,6 +175,10 @@ private boolean isGetRequest(HttpServletRequest request) { return RequestMethod.GET == RequestMethod.valueOf(request.getMethod()); } + private boolean isPostRequest(HttpServletRequest request) { + return RequestMethod.POST == RequestMethod.valueOf(request.getMethod()); + } + private boolean isNonStandardPort(HttpServletRequest request) { return (SCHEME_HTTP.equals(request.getScheme()) && request.getRemotePort() != STANDARD_PORT_HTTP) || (SCHEME_HTTPS.equals(request.getScheme()) && request diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 367350284..cdf7a9b86 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -147,6 +147,39 @@ public void requestWithUrlEncodedParameter() throws IOException { hasItem("$ curl http://localhost/foo?k1=foo+bar%26 -i")); } + @Test + public void postRequestWithOneParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + request.addParameter("k1", "v1"); + documentCurlRequest("post-request-with-one-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("post-request-with-one-parameter"), + hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=v1'")); + } + + @Test + public void postRequestWithMultipleParameters() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + request.addParameter("k1", "v1"); + request.addParameter("k2", "v2"); + request.addParameter("k1", "v1-bis"); + documentCurlRequest("post-request-with-multiple-parameters").handle( + new StubMvcResult(request, null)); + assertThat( + requestSnippetLines("post-request-with-multiple-parameters"), + hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=v1\\&k1=v1-bis\\&k2=v2'")); + } + + @Test + public void postRequestWithUrlEncodedParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + request.addParameter("k1", "a&b"); + documentCurlRequest("post-request-with-url-encoded-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("post-request-with-url-encoded-parameter"), + hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=a%26b'")); + } + @Test public void requestWithHeaders() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); From ecfc7abd922fd8a3e7cc91f87ec541e6d189fef3 Mon Sep 17 00:00:00 2001 From: Andreas Evers Date: Sun, 22 Feb 2015 23:48:29 +0100 Subject: [PATCH 0038/1059] Initial support for documenting fields in requests and responses This commit introduces support for documenting the fields in a request and response, modelled on the existing support for documenting links. A test with field documentation enabled will fail if a documented field is missing from the request or response, or if a field present in the request or response has not been documented. See gh-24 Closes gh-25 --- .../RestDocumentationResultHandler.java | 44 ++++++ .../hypermedia/HypermediaDocumentation.java | 1 - .../springframework/restdocs/state/Field.java | 95 ++++++++++++ .../restdocs/state/FieldDescriptor.java | 105 +++++++++++++ .../restdocs/state/FieldExtractor.java | 86 +++++++++++ .../state/FieldSnippetResultHandler.java | 98 ++++++++++++ .../springframework/restdocs/state/Path.java | 124 ++++++++++++++++ .../restdocs/state/StateDocumentation.java | 94 ++++++++++++ .../state/StateDocumentationValidator.java | 75 ++++++++++ .../restdocs/state/FieldExtractorTests.java | 139 ++++++++++++++++++ .../StateDocumentationValidatorTests.java | 93 ++++++++++++ ...ultiple-fields-and-embedded-and-links.json | 9 ++ .../multiple-fields-and-embedded.json | 5 + .../multiple-fields-and-links.json | 8 + .../field-payloads/multiple-fields.json | 19 +++ .../resources/field-payloads/no-fields.json | 1 + .../field-payloads/single-field.json | 3 + 17 files changed, 998 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java create mode 100644 spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json create mode 100644 spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json create mode 100644 spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json create mode 100644 spring-restdocs/src/test/resources/field-payloads/multiple-fields.json create mode 100644 spring-restdocs/src/test/resources/field-payloads/no-fields.json create mode 100644 spring-restdocs/src/test/resources/field-payloads/single-field.json 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 571f2bec1..bcf3675d5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -20,6 +20,8 @@ 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 java.util.ArrayList; import java.util.List; @@ -28,6 +30,9 @@ 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.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -35,6 +40,7 @@ * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. * * @author Andy Wilkinson + * @author Andreas Evers * @see RestDocumentation#document(String) */ public class RestDocumentationResultHandler implements ResultHandler { @@ -97,4 +103,42 @@ public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, return this; } + /** + * Document the fields in the response using the given {@code descriptors}. The fields + * are extracted from the response based on its content type. + *

+ * 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. + * + * @param descriptors the link descriptors + * @return {@code this} + * @see StateDocumentation#fieldWithPath(Path) + */ + public RestDocumentationResultHandler withRequestFields( + FieldDescriptor... descriptors) { + this.delegates.add(documentRequestFields(this.outputDir, descriptors)); + return this; + } + + /** + * Document the fields in the response using the given {@code descriptors}. The fields + * are extracted from the response based on its content type. + *

+ * 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. + * + * @param descriptors the link descriptors + * @return {@code this} + * @see StateDocumentation#fieldWithPath(Path) + */ + public RestDocumentationResultHandler withResponseFields( + FieldDescriptor... descriptors) { + this.delegates.add(documentResponseFields(this.outputDir, descriptors)); + return this; + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 65a51bd09..2393fa628 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -59,5 +59,4 @@ public static LinkSnippetResultHandler documentLinks(String outputDir, return new LinkSnippetResultHandler(outputDir, linkExtractor, Arrays.asList(descriptors)); } - } 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 new file mode 100644 index 000000000..e0b124fe6 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java @@ -0,0 +1,95 @@ +/* + * 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/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java new file mode 100644 index 000000000..8b9cc9ffc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java @@ -0,0 +1,105 @@ +/* + * 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; + +/** + * A description of a field found in a hypermedia API + * + * @see StateDocumentation#fieldWithPath(Path) + * + * @author Andreas Evers + */ +public class FieldDescriptor { + + private final Path path; + + private String type; + + private boolean required; + + private String constraints; + + private String description; + + FieldDescriptor(Path path) { + this.path = path; + } + + /** + * Specifies the type of the field + * + * @param type The field's type (could be number, string, boolean, array, object, ...) + * @return {@code this} + */ + public FieldDescriptor type(String type) { + this.type = type; + return this; + } + + /** + * Specifies necessity of the field + * + * @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; + return this; + } + + /** + * Specifies the description of the field + * + * @param description The field's description + * @return {@code this} + */ + public FieldDescriptor description(String description) { + this.description = description; + return this; + } + + Path getPath() { + return this.path; + } + + String getType() { + return this.type; + } + + boolean isRequired() { + return this.required; + } + + String getConstraints() { + return this.constraints; + } + + String getDescription() { + return this.description; + } +} 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 new file mode 100644 index 000000000..a46b2f872 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java @@ -0,0 +1,86 @@ +/* + * 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 new file mode 100644 index 000000000..b77ae64d2 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java @@ -0,0 +1,98 @@ +/* + * 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 new file mode 100644 index 000000000..a82557ac7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java @@ -0,0 +1,124 @@ +/* + * 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/StateDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java new file mode 100644 index 000000000..79777587e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java @@ -0,0 +1,94 @@ +/* + * 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 static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.RESPONSE; +import static org.springframework.restdocs.state.Path.path; + +import java.util.Arrays; + +import org.springframework.restdocs.RestDocumentationResultHandler; + +/** + * Static factory methods for documenting a RESTful API's state. + * + * @author Andreas Evers + */ +public abstract class StateDocumentation { + + private StateDocumentation() { + + } + + /** + * Creates a {@code FieldDescriptor} that describes a field with the given + * {@code path}. + * + * @param path The path of the field + * @return a {@code FieldDescriptor} ready for further configuration + * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) + * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) + */ + public static FieldDescriptor fieldWithPath(Path 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. + * + * @param outputDir The directory to which the snippet should be written + * @param descriptors The descriptions of the request's fields + * @return the handler + * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) + */ + public static FieldSnippetResultHandler documentRequestFields(String outputDir, + FieldDescriptor... descriptors) { + return new FieldSnippetResultHandler(outputDir, REQUEST, + Arrays.asList(descriptors)); + } + + /** + * Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a + * documentation snippet for a response's fields. + * + * @param outputDir The directory to which the snippet should be written + * @param descriptors The descriptions of the response's fields + * @return the handler + * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) + */ + public static FieldSnippetResultHandler documentResponseFields(String outputDir, + FieldDescriptor... descriptors) { + return new FieldSnippetResultHandler(outputDir, RESPONSE, + Arrays.asList(descriptors)); + } + +} 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 new file mode 100644 index 000000000..60409cbf4 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java @@ -0,0 +1,75 @@ +/* + * 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/state/FieldExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java new file mode 100644 index 000000000..a8533bbe7 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java @@ -0,0 +1,139 @@ +/* + * 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 new file mode 100644 index 000000000..38c635058 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java @@ -0,0 +1,93 @@ +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); + } +} diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json new file mode 100644 index 000000000..fdaf197d7 --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json @@ -0,0 +1,9 @@ +{ + "_links": { + "alpha": "http://alpha.example.com", + "bravo": "http://bravo.example.com" + }, + "_embedded": "embedded-test", + "beta": "beta-value", + "charlie": "charlie-value" +} \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json new file mode 100644 index 000000000..0d4e6b0ac --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json @@ -0,0 +1,5 @@ +{ + "_embedded": "embedded-test", + "beta": "beta-value", + "charlie": "charlie-value" +} \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json new file mode 100644 index 000000000..794ece500 --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json @@ -0,0 +1,8 @@ +{ + "_links": { + "alpha": "http://alpha.example.com", + "bravo": "http://bravo.example.com" + }, + "beta": "beta-value", + "charlie": "charlie-value" +} \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields.json b/spring-restdocs/src/test/resources/field-payloads/multiple-fields.json new file mode 100644 index 000000000..a4d2b54ac --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/multiple-fields.json @@ -0,0 +1,19 @@ +{ + "alpha": "alpha-value", + "bravo": 123, + "charlie": { + "one": 456, + "two": "two-value" + }, + "delta": [ + "delta-value-1", + "delta-value-2" + ], + "echo": [{ + "one": 789, + "two": "two-value" + },{ + "one": 987, + "two": "value-two" + }] +} \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/field-payloads/no-fields.json b/spring-restdocs/src/test/resources/field-payloads/no-fields.json new file mode 100644 index 000000000..6f31cf5a2 --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/no-fields.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/field-payloads/single-field.json b/spring-restdocs/src/test/resources/field-payloads/single-field.json new file mode 100644 index 000000000..c209f4435 --- /dev/null +++ b/spring-restdocs/src/test/resources/field-payloads/single-field.json @@ -0,0 +1,3 @@ +{ + "alpha": "alpha-value" +} \ No newline at end of file From 2b44452de1661efe51276b6cb396bac77b9e5f08 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Mar 2015 18:03:17 +0000 Subject: [PATCH 0039/1059] 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 | 149 ++++-------------- .../com/example/notes/ApiDocumentation.java | 77 ++++++--- .../src/main/asciidoc/api-guide.asciidoc | 118 +++----------- .../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, 847 insertions(+), 1046 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 00708d67a..63151d080 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,22 +223,17 @@ 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.asciidoc[] +include::{generated}/note-get-example/response-fields.asciidoc[] -|=== -| JSON path | Description +==== Example request -| `title` -| The title of the note +include::{generated}/note-get-example/request.asciidoc[] -| `body` -| The body of the note +==== Example response -| `_links` -| <> to other resources -|=== +include::{generated}/note-get-example/response.asciidoc[] @@ -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,19 +277,17 @@ 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.asciidoc[] +include::{generated}/tag-get-example/response-fields.asciidoc[] -|=== -| JSON path | Description +==== Example request -| `name` -| The name of the tag +include::{generated}/tag-get-example/request.asciidoc[] -| `_links` -| <> to other resources -|=== +==== Example response + +include::{generated}/tag-get-example/response.asciidoc[] @@ -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 e5902a612..7e1f79337 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.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.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 void errorExample() throws Exception { .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 void notesListExample() throws Exception { 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 void notesCreateExample() throws Exception { 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 void noteGetExample() throws Exception { .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 void tagsListExample() throws Exception { 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 void tagsCreateExample() throws Exception { 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 void noteUpdateExample() throws Exception { 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 void tagGetExample() throws Exception { 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 void tagUpdateExample() throws Exception { 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 fb83fc5fd..35edc0f3a 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,22 +223,17 @@ 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.asciidoc[] +include::{generated}/note-get-example/response-fields.asciidoc[] -|=== -| JSON path | Description +==== Example request -| `title` -| The title of the note +include::{generated}/note-get-example/request.asciidoc[] -| `body` -| The body of the note +==== Example response -| `_links` -| <> to other resources -|=== +include::{generated}/note-get-example/response.asciidoc[] @@ -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,19 +276,17 @@ 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.asciidoc[] +include::{generated}/tag-get-example/response-fields.asciidoc[] -|=== -| JSON path | Description +==== Example request -| `name` -| The name of the tag +include::{generated}/tag-get-example/request.asciidoc[] -| `_links` -| <> to other resources -|=== +==== Example response + +include::{generated}/tag-get-example/response.asciidoc[] @@ -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 bcb731bf0..f8ce6fa18 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.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.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 void errorExample() throws Exception { .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 void notesListExample() throws Exception { 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 void notesCreateExample() throws Exception { 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 void noteGetExample() throws Exception { .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 void tagsListExample() throws Exception { 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 void tagsCreateExample() throws Exception { 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 void noteUpdateExample() throws Exception { 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 void tagGetExample() throws Exception { 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 void tagUpdateExample() throws Exception { 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 bcf3675d5..bd3b63595 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.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.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 RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, } /** - * 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 RestDocumentationResultHandler withRequestFields( } /** - * 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 RestDocumentationResultHandler withRequestFields( * * @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 8b9cc9ffc..2c81b9eda 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, ...) - * @return {@code this} - */ - public FieldDescriptor type(String type) { - this.type = type; - return this; - } - - /** - * Specifies necessity of the field + * @param type The type of the field * - * @param required The field's necessity * @return {@code this} */ - public FieldDescriptor required(boolean required) { - this.required = required; + public FieldDescriptor type(FieldType type) { + this.type = type; return this; } /** - * Specifies the constraints of the field + * Marks the field as optional * - * @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 FieldDescriptor description(String description) { 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 000000000..d3363662f --- /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 000000000..8444bc3de --- /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 000000000..e93f08c1e --- /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 000000000..052aa8e72 --- /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 000000000..19599afc6 --- /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 79777587e..774d23a05 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 @@ private 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 static FieldDescriptor fieldWithPath(String name) { */ 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 static FieldSnippetResultHandler documentRequestFields(String outputDir, */ 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 000000000..b589ef8b4 --- /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 000000000..af8e3d1f0 --- /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 e0b124fe6..000000000 --- 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 a46b2f872..000000000 --- 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 b77ae64d2..000000000 --- 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 a82557ac7..000000000 --- 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 60409cbf4..000000000 --- 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 000000000..667225f8e --- /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 000000000..9af51c0e1 --- /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 a8533bbe7..000000000 --- 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 38c635058..000000000 --- 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); - } -} From 0d2d9cce73531d164562b395451982176e481c40 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 Mar 2015 17:06:36 +0000 Subject: [PATCH 0040/1059] Polishing --- .../springframework/restdocs/payload/FieldValidator.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 19599afc6..469d0f6cd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -68,10 +68,9 @@ private List findMissingFields(Map payload, List missingFields = new ArrayList(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional()) { - if (!this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) { - missingFields.add(fieldDescriptor.getPath()); - } + if (!fieldDescriptor.isOptional() + && !this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) { + missingFields.add(fieldDescriptor.getPath()); } } From 976dd00365cf3151ee17970b0e44acd06f6975ab Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 Mar 2015 17:31:04 +0000 Subject: [PATCH 0041/1059] Use TableWriter in LinkSnippetResultHandler Closes gh-40 --- .../hypermedia/LinkSnippetResultHandler.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index bc4441c1f..d2c1f2c01 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -27,6 +27,8 @@ import java.util.Set; 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; @@ -100,16 +102,19 @@ protected void handle(MvcResult result, DocumentationWriter writer) Assert.isTrue(actualRels.equals(expectedRels)); - writer.println("|==="); - writer.println("| Relation | Description"); + writer.table(new TableAction() { - for (Entry entry : this.descriptorsByRel.entrySet()) { - writer.println(); - writer.println("| " + entry.getKey()); - writer.println("| " + entry.getValue().getDescription()); - } + @Override + public void perform(TableWriter tableWriter) throws IOException { + tableWriter.headers("Relation", "Description"); + for (Entry entry : LinkSnippetResultHandler.this.descriptorsByRel + .entrySet()) { + tableWriter.row(entry.getKey(), entry.getValue().getDescription()); + } + } + + }); - writer.println("|==="); } } \ No newline at end of file From 86b431018d6fc99d168af162cd53275d9e92e5c4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2015 14:40:27 +0000 Subject: [PATCH 0042/1059] Apply custom host and port configuration correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, a custom host was configured by setting the request’s remote host. This is in correct as the remote host as the client’s host. The custom port was also being configured by setting the request’s remote port and server port. Only the latter is required. This commit correct the logic by setting the request’s server name and server port when configuring a custom host and custom port respectively. The tests have been improved by introducing the use of Spring HATEOAS’s BasicLinkBuilder to verify that the configured request produces the expected scheme, host and port in any generated links. Closes gh-44 --- build.gradle | 2 ++ .../config/RestDocumentationConfigurer.java | 3 +-- .../restdocs/curl/CurlDocumentation.java | 8 ++++---- .../RestDocumentationConfigurerTests.java | 20 ++++++++++++++++--- .../restdocs/curl/CurlDocumentationTests.java | 16 ++++++++++++--- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 45429aa91..10159e0cc 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ project(':spring-restdocs') { jacocoVersion = '0.7.2.201409121644' junitVersion = '4.11' servletApiVersion = '3.1.0' + springHateoasVersion = '0.17.0.RELEASE' springVersion = '4.1.4.RELEASE' mockitoVersion = '1.10.19' } @@ -64,6 +65,7 @@ project(':spring-restdocs') { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" testCompile "org.springframework:spring-webmvc:$springVersion" + testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion" testCompile "org.mockito:mockito-core:$mockitoVersion" } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 44f805115..481d26ea9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -106,9 +106,8 @@ public MockHttpServletRequest postProcessRequest( currentContext.getAndIncrementStepCount(); } request.setScheme(RestDocumentationConfigurer.this.scheme); - request.setRemotePort(RestDocumentationConfigurer.this.port); request.setServerPort(RestDocumentationConfigurer.this.port); - request.setRemoteHost(RestDocumentationConfigurer.this.host); + request.setServerName(RestDocumentationConfigurer.this.host); return request; } }; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index ee7f3cb99..afd3b86d2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -134,10 +134,10 @@ private static final class CurlRequestDocumentationAction implements public void perform() throws IOException { MockHttpServletRequest request = this.result.getRequest(); this.writer.print(String.format("curl %s://%s", request.getScheme(), - request.getRemoteHost())); + request.getServerName())); if (isNonStandardPort(request)) { - this.writer.print(String.format(":%d", request.getRemotePort())); + this.writer.print(String.format(":%d", request.getServerPort())); } this.writer.print(getRequestUriWithQueryString(request)); @@ -180,9 +180,9 @@ private boolean isPostRequest(HttpServletRequest request) { } private boolean isNonStandardPort(HttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getRemotePort() != STANDARD_PORT_HTTP) + return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) || (SCHEME_HTTPS.equals(request.getScheme()) && request - .getRemotePort() != STANDARD_PORT_HTTPS); + .getServerPort() != STANDARD_PORT_HTTPS); } private String getRequestUriWithQueryString(HttpServletRequest request) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index a4a938288..24b76da91 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -18,10 +18,14 @@ import static org.junit.Assert.assertEquals; +import java.net.URI; + import org.junit.Test; +import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; /** * Tests for {@link RestDocumentationConfigurer}. @@ -70,9 +74,19 @@ public void customPort() { private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); - assertEquals(host, this.request.getRemoteHost()); - assertEquals(port, this.request.getRemotePort()); + assertEquals(host, this.request.getServerName()); assertEquals(port, this.request.getServerPort()); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes( + this.request)); + try { + URI uri = BasicLinkBuilder.linkToCurrentMapping().toUri(); + assertEquals(scheme, uri.getScheme()); + assertEquals(host, uri.getHost()); + assertEquals(port, uri.getPort()); + } + finally { + RequestContextHolder.resetRequestAttributes(); + } } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index cdf7a9b86..7303adce0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -256,7 +256,7 @@ public void requestAndResponse() throws IOException { @Test public void httpWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setRemotePort(8080); + request.setServerPort(8080); documentCurlRequest("http-with-non-standard-port").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("http-with-non-standard-port"), @@ -266,7 +266,7 @@ public void httpWithNonStandardPort() throws IOException { @Test public void httpsWithStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setRemotePort(443); + request.setServerPort(443); request.setScheme("https"); documentCurlRequest("https-with-standard-port").handle( new StubMvcResult(request, null)); @@ -277,7 +277,7 @@ public void httpsWithStandardPort() throws IOException { @Test public void httpsWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setRemotePort(8443); + request.setServerPort(8443); request.setScheme("https"); documentCurlRequest("https-with-non-standard-port").handle( new StubMvcResult(request, null)); @@ -285,6 +285,16 @@ public void httpsWithNonStandardPort() throws IOException { hasItem("$ curl https://localhost:8443/foo -i")); } + @Test + public void requestWithCustomHost() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setServerName("api.example.com"); + documentCurlRequest("request-with-custom-host").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-custom-host"), + hasItem("$ curl http://api.example.com/foo -i")); + } + private List requestSnippetLines(String snippetName) throws IOException { return snippetLines(snippetName, "request"); } From e00734d693ba07f7149843fe8e6ff5851c092fda Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2015 13:49:44 +0000 Subject: [PATCH 0043/1059] Add support for generating an HTTP request documentation snippet Closes gh-13 --- README.md | 10 +- .../src/main/asciidoc/api-guide.asciidoc | 36 +-- .../asciidoc/getting-started-guide.asciidoc | 44 ++-- .../src/main/asciidoc/api-guide.asciidoc | 36 +-- .../asciidoc/getting-started-guide.asciidoc | 44 ++-- .../RestDocumentationResultHandler.java | 8 +- .../config/RestDocumentationConfigurer.java | 12 + .../restdocs/curl/CurlConfiguration.java | 35 --- .../restdocs/curl/CurlDocumentation.java | 210 +++-------------- .../curl/CurlSnippetResultHandler.java | 51 ---- .../restdocs/http/HttpDocumentation.java | 155 ++++++++++++ .../util/DocumentableHttpServletRequest.java | 223 ++++++++++++++++++ .../RestDocumentationIntegrationTests.java | 21 +- .../RestDocumentationConfigurerTests.java | 23 ++ .../restdocs/curl/CurlDocumentationTests.java | 87 +------ .../restdocs/http/HttpDocumentationTests.java | 175 ++++++++++++++ 16 files changed, 725 insertions(+), 445 deletions(-) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java diff --git a/README.md b/README.md index 9bacefee6..1bc9b39d2 100644 --- a/README.md +++ b/README.md @@ -279,9 +279,9 @@ command for the request and the resulting response to files in a directory named `index` in the project's `build/generated-snippets/` directory. Three files will be written: - - `index/request.asciidoc` - - `index/response.asciidoc` - - `index/request-response.asciidoc` + - `index/curl-request.asciidoc` + - `index/http-request.asciidoc` + - `index/http-response.asciidoc` #### Parameterized output directories @@ -340,8 +340,8 @@ which the snippets are written. For example, to include both the request and res snippets described above: ``` -include::{generated}/index/request.asciidoc[] -include::{generated}/index/response.asciidoc[] +include::{generated}/index/curl-request.asciidoc[] +include::{generated}/index/http-response.asciidoc[] ``` ## Generating snippets in your IDE 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 63151d080..48a32aad6 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 @@ -69,7 +69,7 @@ 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: -include::{generated}/error-example/response.asciidoc[] +include::{generated}/error-example/http-response.asciidoc[] [[overview-hypermedia]] == Hypermedia @@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[] ==== Example response -include::{generated}/index-example/response.asciidoc[] +include::{generated}/index-example/http-response.asciidoc[] @@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/notes-list-example/request.asciidoc[] +include::{generated}/notes-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-list-example/response.asciidoc[] +include::{generated}/notes-list-example/http-response.asciidoc[] @@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/notes-create-example/request.asciidoc[] +include::{generated}/notes-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-create-example/response.asciidoc[] +include::{generated}/notes-create-example/http-response.asciidoc[] @@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tags-list-example/request.asciidoc[] +include::{generated}/tags-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-list-example/response.asciidoc[] +include::{generated}/tags-list-example/http-response.asciidoc[] @@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tags-create-example/request.asciidoc[] +include::{generated}/tags-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-create-example/response.asciidoc[] +include::{generated}/tags-create-example/http-response.asciidoc[] @@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/note-get-example/request.asciidoc[] +include::{generated}/note-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-get-example/response.asciidoc[] +include::{generated}/note-get-example/http-response.asciidoc[] @@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from ==== Example request -include::{generated}/note-update-example/request.asciidoc[] +include::{generated}/note-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-update-example/response.asciidoc[] +include::{generated}/note-update-example/http-response.asciidoc[] @@ -283,11 +283,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tag-get-example/request.asciidoc[] +include::{generated}/tag-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-get-example/response.asciidoc[] +include::{generated}/tag-get-example/http-response.asciidoc[] @@ -302,8 +302,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tag-update-example/request.asciidoc[] +include::{generated}/tag-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-update-example/response.asciidoc[] +include::{generated}/tag-update-example/http-response.asciidoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc index e7781f242..c9eac9528 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/request.asciidoc[] +include::{generated}/index/1/curl-request.asciidoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a note, you need to execute a `POST` request to this URI including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example, you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] Note the `tags` link which we'll make use of later. @@ -92,26 +92,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.asciidoc[] @@ -132,24 +132,25 @@ with it. Once again we execute a `POST` request. However, this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.asciidoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/creating-a-note/5/response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/request-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.asciidoc[] +include::{generated}/creating-a-note/6/http-response.asciidoc[] To verify that the tag has been associated with the note, we can perform a `GET` request against the URI from the `tags` link: -include::{generated}/creating-a-note/7/request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.asciidoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/creating-a-note/7/response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.asciidoc[] @@ -159,17 +160,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll used the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/request-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.asciidoc[] +include::{generated}/creating-a-note/9/http-response.asciidoc[] 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 35edc0f3a..3919f86ce 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 @@ -69,7 +69,7 @@ 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: -include::{generated}/error-example/response.asciidoc[] +include::{generated}/error-example/http-response.asciidoc[] [[overview-hypermedia]] == Hypermedia @@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[] ==== Example response -include::{generated}/index-example/response.asciidoc[] +include::{generated}/index-example/http-response.asciidoc[] @@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/notes-list-example/request.asciidoc[] +include::{generated}/notes-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-list-example/response.asciidoc[] +include::{generated}/notes-list-example/http-response.asciidoc[] @@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/notes-create-example/request.asciidoc[] +include::{generated}/notes-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-create-example/response.asciidoc[] +include::{generated}/notes-create-example/http-response.asciidoc[] @@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tags-list-example/request.asciidoc[] +include::{generated}/tags-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-list-example/response.asciidoc[] +include::{generated}/tags-list-example/http-response.asciidoc[] @@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tags-create-example/request.asciidoc[] +include::{generated}/tags-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-create-example/response.asciidoc[] +include::{generated}/tags-create-example/http-response.asciidoc[] @@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/note-get-example/request.asciidoc[] +include::{generated}/note-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-get-example/response.asciidoc[] +include::{generated}/note-get-example/http-response.asciidoc[] @@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from ==== Example request -include::{generated}/note-update-example/request.asciidoc[] +include::{generated}/note-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-update-example/response.asciidoc[] +include::{generated}/note-update-example/http-response.asciidoc[] [[resources-tag]] @@ -282,11 +282,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tag-get-example/request.asciidoc[] +include::{generated}/tag-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-get-example/response.asciidoc[] +include::{generated}/tag-get-example/http-response.asciidoc[] @@ -301,8 +301,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tag-update-example/request.asciidoc[] +include::{generated}/tag-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-update-example/response.asciidoc[] +include::{generated}/tag-update-example/http-response.asciidoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc index 8fccc65c6..9134e8521 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/request.asciidoc[] +include::{generated}/index/1/curl-request.asciidoc[] This request should yield the following response: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a note you need to execute a `POST` request to this URI, including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.asciidoc[] @@ -131,24 +131,25 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.asciidoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/creating-a-note/5/response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/request-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.asciidoc[] +include::{generated}/creating-a-note/6/http-response.asciidoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/creating-a-note/7/request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.asciidoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/creating-a-note/7/response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.asciidoc[] @@ -158,17 +159,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.asciidoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/request-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.asciidoc[] +include::{generated}/creating-a-note/9/http-response.asciidoc[] 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 bd3b63595..11b555348 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -17,8 +17,8 @@ package org.springframework.restdocs; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; @@ -53,8 +53,8 @@ public class RestDocumentationResultHandler implements ResultHandler { this.delegates = new ArrayList(); this.delegates.add(documentCurlRequest(this.outputDir)); - this.delegates.add(documentCurlResponse(this.outputDir)); - this.delegates.add(documentCurlRequestAndResponse(this.outputDir)); + this.delegates.add(documentHttpRequest(this.outputDir)); + this.delegates.add(documentHttpResponse(this.outputDir)); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 481d26ea9..57b37f983 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,6 +21,7 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; /** @@ -108,8 +109,19 @@ public MockHttpServletRequest postProcessRequest( request.setScheme(RestDocumentationConfigurer.this.scheme); request.setServerPort(RestDocumentationConfigurer.this.port); request.setServerName(RestDocumentationConfigurer.this.host); + configureContentLengthHeaderIfAppropriate(request); return request; } + + private void configureContentLengthHeaderIfAppropriate( + MockHttpServletRequest request) { + long contentLength = request.getContentLengthLong(); + if (contentLength > 0 + && !StringUtils.hasText(request.getHeader("Content-Length"))) { + request.addHeader("Content-Length", request.getContentLengthLong()); + } + } + }; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java deleted file mode 100644 index c8c8c9441..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java +++ /dev/null @@ -1,35 +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.curl; - -/** - * Configuration for documenting Curl requests and responses - * - * @author Andy Wilkinson - */ -class CurlConfiguration { - - private boolean includeResponseHeaders = true; - - boolean isIncludeResponseHeaders() { - return this.includeResponseHeaders; - } - - void setIncludeResponseHeaders(boolean includeResponseHeaders) { - this.includeResponseHeaders = includeResponseHeaders; - } -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index afd3b86d2..960ba88ca 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -16,29 +16,21 @@ package org.springframework.restdocs.curl; -import static org.springframework.restdocs.util.IterableEnumeration.iterable; - import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map.Entry; -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.snippet.DocumentationWriter; import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; /** * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. - * + * * @author Andy Wilkinson * @author Yann Le Guern */ @@ -50,58 +42,17 @@ private CurlDocumentation() { /** * Produces a documentation snippet containing the request formatted as a cURL command - * + * * @param outputDir The directory to which snippet should be written * @return the handler that will produce the snippet */ - public static CurlSnippetResultHandler documentCurlRequest(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - } - }; - } - - /** - * Produces a documentation snippet containing the response formatted as the response - * to a cURL command - * - * @param outputDir The directory to which snippet should be written - * @return the handler that will produce the snippet - */ - public static CurlSnippetResultHandler documentCurlResponse(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "response") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); - } - }; - } - - /** - * Produces a documentation snippet containing both the request formatted as a cURL - * command and the response formatted formatted s the response to a cURL command. - * - * @param outputDir The directory to which the snippet should be written - * @return the handler that will produce the snippet - */ - public static CurlSnippetResultHandler documentCurlRequestAndResponse(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "request-response") { + public static SnippetWritingResultHandler documentCurlRequest(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "curl-request") { @Override public void handle(MvcResult result, DocumentationWriter writer) throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); + writer.shellCommand(new CurlRequestDocumentationAction(writer, result)); } }; } @@ -121,152 +72,55 @@ private static final class CurlRequestDocumentationAction implements private final MvcResult result; - private final CurlConfiguration curlConfiguration; - - CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { + CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { this.writer = writer; this.result = result; - this.curlConfiguration = curlConfiguration; } @Override public void perform() throws IOException { - MockHttpServletRequest request = this.result.getRequest(); + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + this.result.getRequest()); this.writer.print(String.format("curl %s://%s", request.getScheme(), - request.getServerName())); + request.getHost())); if (isNonStandardPort(request)) { - this.writer.print(String.format(":%d", request.getServerPort())); + this.writer.print(String.format(":%d", request.getPort())); } - this.writer.print(getRequestUriWithQueryString(request)); + this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&")); - if (this.curlConfiguration.isIncludeResponseHeaders()) { - this.writer.print(" -i"); - } + this.writer.print(" -i"); - if (!isGetRequest(request)) { + if (!request.isGetRequest()) { this.writer.print(String.format(" -X %s", request.getMethod())); } - for (String headerName : iterable(request.getHeaderNames())) { - for (String header : iterable(request.getHeaders(headerName))) { - this.writer - .print(String.format(" -H \"%s: %s\"", headerName, header)); + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + this.writer.print(String.format(" -H \"%s: %s\"", entry.getKey(), + header)); } } - if (request.getContentLengthLong() > 0) { - this.writer.print(String.format(" -d '%s'", getContent(request))); + if (request.getContentLength() > 0) { + this.writer + .print(String.format(" -d '%s'", request.getContentAsString())); } - else if (isPostRequest(request)) { - Map parameters = request.getParameterMap(); - if (!parameters.isEmpty()) { - this.writer.print(String - .format(" -d '%s'", toQueryString(parameters))); + else if (request.isPostRequest()) { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + this.writer.print(String.format(" -d '%s'", + queryString.replace("&", "\\&"))); } } this.writer.println(); } - private boolean isGetRequest(HttpServletRequest request) { - return RequestMethod.GET == RequestMethod.valueOf(request.getMethod()); - } - - private boolean isPostRequest(HttpServletRequest request) { - return RequestMethod.POST == RequestMethod.valueOf(request.getMethod()); - } - - private boolean isNonStandardPort(HttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request - .getServerPort() != STANDARD_PORT_HTTPS); - } - - private String getRequestUriWithQueryString(HttpServletRequest request) { - StringBuilder sb = new StringBuilder(); - sb.append(request.getRequestURI()); - String queryString = getQueryString(request); - if (StringUtils.hasText(queryString)) { - sb.append('?').append(queryString); - } - return sb.toString(); - } - - private String getQueryString(HttpServletRequest request) { - if (request.getQueryString() != null) { - return request.getQueryString().replace("&", "\\&"); - } - if (isGetRequest(request)) { - return toQueryString(request.getParameterMap()); - } - return null; - } - - private static String toQueryString(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - for (String value : entry.getValue()) { - if (sb.length() > 0) { - sb.append("\\&"); - } - sb.append(urlEncodeUTF8(entry.getKey())).append('=') - .append(urlEncodeUTF8(value)); - } - } - return sb.toString(); - } - - private static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + s - + " using UTF-8", ex); - } - } - - private String getContent(MockHttpServletRequest request) throws IOException { - StringWriter bodyWriter = new StringWriter(); - FileCopyUtils.copy(request.getReader(), bodyWriter); - return bodyWriter.toString(); - } - } - - private static final class CurlResponseDocumentationAction implements - DocumentationAction { - - private final DocumentationWriter writer; - - private final MvcResult result; - - private final CurlConfiguration curlConfiguration; - - CurlResponseDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { - this.writer = writer; - this.result = result; - this.curlConfiguration = curlConfiguration; - } - - @Override - public void perform() throws IOException { - if (this.curlConfiguration.isIncludeResponseHeaders()) { - HttpStatus status = HttpStatus.valueOf(this.result.getResponse() - .getStatus()); - this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), - status.getReasonPhrase())); - for (String headerName : this.result.getResponse().getHeaderNames()) { - for (String header : this.result.getResponse().getHeaders(headerName)) { - this.writer.println(String.format("%s: %s", headerName, header)); - } - } - this.writer.println(); - } - this.writer.println(this.result.getResponse().getContentAsString()); + private boolean isNonStandardPort(DocumentableHttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java deleted file mode 100644 index b4ba9bc43..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java +++ /dev/null @@ -1,51 +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.curl; - -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.test.web.servlet.ResultHandler; - -/** - * Abstract base class for Spring Mock MVC {@link ResultHandler ResultHandlers} that - * produce documentation snippets relating to cURL. - * - * @author Andy Wilkinson - */ -public abstract class CurlSnippetResultHandler extends SnippetWritingResultHandler { - - private final CurlConfiguration curlConfiguration = new CurlConfiguration(); - - public CurlSnippetResultHandler(String outputDir, String fileName) { - super(outputDir, fileName); - } - - CurlConfiguration getCurlConfiguration() { - return this.curlConfiguration; - } - - /** - * Specify whether or not the generated cURL snippets should have contents as if cURL - * had been invoked with {@code -i, --include}. - * - * @param include {@code true} to use {@code -i, --include}, otherwise false - * @return {@code this} - */ - public CurlSnippetResultHandler includeResponseHeaders(boolean include) { - this.curlConfiguration.setIncludeResponseHeaders(include); - return this; - } -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java new file mode 100644 index 000000000..d840f925a --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -0,0 +1,155 @@ +/* + * 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.http; + +import java.io.IOException; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.DocumentationWriter; +import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.util.DocumentableHttpServletRequest; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StringUtils; + +/** + * Static factory methods for documenting a RESTful API's HTTP requests. + * + * @author Andy Wilkinson + */ +public abstract class HttpDocumentation { + + private HttpDocumentation() { + + } + + /** + * Produces a documentation snippet containing the request formatted as an HTTP + * request + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static SnippetWritingResultHandler documentHttpRequest(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "http-request") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws IOException { + writer.codeBlock("http", new HttpRequestDocumentationAction(writer, + result)); + } + }; + } + + /** + * Produces a documentation snippet containing the response formatted as the HTTP + * response sent by the server + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static SnippetWritingResultHandler documentHttpResponse(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "http-response") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws IOException { + writer.codeBlock("http", new HttpResponseDocumentationAction(writer, + result)); + } + }; + } + + private static class HttpRequestDocumentationAction implements DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + HttpRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { + this.writer = writer; + this.result = result; + } + + @Override + public void perform() throws IOException { + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + this.result.getRequest()); + this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), + request.getRequestUriWithQueryString()); + for (Entry> header : request.getHeaders().entrySet()) { + for (String value : header.getValue()) { + this.writer.printf("%s: %s%n", header.getKey(), value); + } + } + if (requiresFormEncodingContentType(request)) { + this.writer.printf("%s: %s%n", HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + this.writer.println(); + if (request.getContentLength() > 0) { + this.writer.println(request.getContentAsString()); + } + else if (request.isPostRequest()) { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + this.writer.println(queryString); + } + } + } + + private boolean requiresFormEncodingContentType( + DocumentableHttpServletRequest request) { + return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null + && request.isPostRequest() + && StringUtils.hasText(request.getParameterMapAsQueryString()); + } + } + + private static final class HttpResponseDocumentationAction implements + DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + HttpResponseDocumentationAction(DocumentationWriter writer, MvcResult result) { + this.writer = writer; + this.result = result; + } + + @Override + public void perform() throws IOException { + HttpStatus status = HttpStatus.valueOf(this.result.getResponse().getStatus()); + this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), + status.getReasonPhrase())); + for (String headerName : this.result.getResponse().getHeaderNames()) { + for (String header : this.result.getResponse().getHeaders(headerName)) { + this.writer.println(String.format("%s: %s", headerName, header)); + } + } + this.writer.println(); + this.writer.println(this.result.getResponse().getContentAsString()); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java new file mode 100644 index 000000000..03fa98a44 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -0,0 +1,223 @@ +/* + * 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.util; + +import static org.springframework.restdocs.util.IterableEnumeration.iterable; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * An {@link HttpServletRequest} wrapper that provides a limited set of methods intended + * to help in the documentation of the request. + * + * @author Andy Wilkinson + * + */ +public class DocumentableHttpServletRequest { + + private final MockHttpServletRequest delegate; + + /** + * Creates a new {@link DocumentableHttpServletRequest} to document the given + * {@code request}. + * + * @param request the request that is to be documented + */ + public DocumentableHttpServletRequest(MockHttpServletRequest request) { + this.delegate = request; + } + + /** + * Whether or not this request is a {@code GET} request. + * + * @return {@code true} if it is a {@code GET} request, otherwise {@code false} + * @see HttpServletRequest#getMethod() + */ + public boolean isGetRequest() { + return RequestMethod.GET == RequestMethod.valueOf(this.delegate.getMethod()); + } + + /** + * Whether or not this request is a {@code POST} request. + * + * @return {@code true} if it is a {@code POST} request, otherwise {@code false} + * @see HttpServletRequest#getMethod() + */ + public boolean isPostRequest() { + return RequestMethod.POST == RequestMethod.valueOf(this.delegate.getMethod()); + } + + /** + * Returns a Map of the request's headers. The entries are ordered based on the + * ordering of {@link HttpServletRequest#getHeaderNames()} and + * {@link HttpServletRequest#getHeaders(String)}. + * + * @return the request's headers, keyed by name + * @see HttpServletRequest#getHeaderNames() + * @see HttpServletRequest#getHeaders(String) + */ + public Map> getHeaders() { + Map> headersByName = new LinkedHashMap>(); + for (String headerName : iterable(this.delegate.getHeaderNames())) { + List headers = new ArrayList(); + headersByName.put(headerName, headers); + for (String header : iterable(this.delegate.getHeaders(headerName))) { + headers.add(header); + } + } + return headersByName; + } + + /** + * Returns the request's scheme. + * + * @return the request's scheme + * @see HttpServletRequest#getScheme() + */ + public String getScheme() { + return this.delegate.getScheme(); + } + + /** + * Returns the name of the host to which the request was sent. + * + * @return the host's name + * @see HttpServletRequest#getServerName() + */ + public String getHost() { + return this.delegate.getServerName(); + } + + /** + * Returns the port to which the request was sent. + * + * @return the port + * @see HttpServletRequest#getServerPort() + */ + public int getPort() { + return this.delegate.getServerPort(); + } + + /** + * Returns the request's method. + * + * @return the request's method + * @see HttpServletRequest#getMethod() + */ + public String getMethod() { + return this.delegate.getMethod(); + } + + /** + * Returns the length of the request's content + * + * @return the content length + * @see HttpServletRequest#getContentLength() + */ + public long getContentLength() { + return this.delegate.getContentLengthLong(); + } + + /** + * Returns a {@code String} of the request's content + * + * @return the request's content + * @throws IOException if the content cannot be read + */ + public String getContentAsString() throws IOException { + StringWriter bodyWriter = new StringWriter(); + FileCopyUtils.copy(this.delegate.getReader(), bodyWriter); + return bodyWriter.toString(); + } + + /** + * Returns the request's URI including its query string. The query string is + * determined by calling {@link HttpServletRequest#getQueryString()}. If it's + * {@code null} and it is a {@code GET} request, the query string is then constructed + * from the request's {@link HttpServletRequest#getParameterMap()} parameter map. + * + * @return the URI of the request, including its query string + */ + public String getRequestUriWithQueryString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.delegate.getRequestURI()); + String queryString = getQueryString(); + if (StringUtils.hasText(queryString)) { + sb.append('?').append(queryString); + } + return sb.toString(); + } + + /** + * Returns the request's parameter map formatted as a query string + * + * @return The query string derived from the request's parameter map + * @see HttpServletRequest#getParameterMap() + */ + public String getParameterMapAsQueryString() { + return toQueryString(this.delegate.getParameterMap()); + } + + private String getQueryString() { + if (this.delegate.getQueryString() != null) { + return this.delegate.getQueryString(); + } + if (isGetRequest()) { + return getParameterMapAsQueryString(); + } + return null; + } + + private static String toQueryString(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + for (String value : entry.getValue()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(urlEncodeUTF8(entry.getKey())).append('=') + .append(urlEncodeUTF8(value)); + } + } + return sb.toString(); + } + + private static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", + ex); + } + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 549b8fc94..b52a6ff1e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -84,7 +84,8 @@ public void basicSnippetGeneration() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic")); assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), - "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } @Test @@ -96,7 +97,8 @@ public void parameterizedOutputDirectory() throws Exception { .andExpect(status().isOk()).andDo(document("{method-name}")); assertExpectedSnippetFilesExist(new File( "build/generated-snippets/parameterized-output-directory"), - "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } @Test @@ -108,20 +110,23 @@ public void multiStep() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-1/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-1/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-2/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-2/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-3/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-3/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 24b76da91..f5c910a30 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -16,7 +16,11 @@ package org.springframework.restdocs.config; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import java.net.URI; @@ -72,6 +76,25 @@ public void customPort() { assertUriConfiguration("http", "localhost", 8081); } + @Test + public void noContentLengthHeaderWhenRequestHasNotContent() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( + 8081).beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + assertThat(this.request.getHeader("Content-Length"), is(nullValue())); + } + + @Test + public void contentLengthHeaderIsSetWhenRequestHasContent() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( + 8081).beforeMockMvcCreated(null, null); + byte[] content = "Hello, world".getBytes(); + this.request.setContent(content); + postProcessor.postProcessRequest(this.request); + assertThat(this.request.getHeader("Content-Length"), + is(equalTo(Integer.toString(content.length)))); + } + private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 7303adce0..24131f713 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -17,12 +17,8 @@ package org.springframework.restdocs.curl; import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; import java.io.BufferedReader; import java.io.File; @@ -34,10 +30,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.StubMvcResult; /** @@ -77,15 +71,6 @@ public void nonGetRequest() throws IOException { hasItem("$ curl http://localhost/foo -i -X POST")); } - @Test - public void requestWithoutResponseHeaderInclusion() throws IOException { - documentCurlRequest("request-without-response-header-inclusion") - .includeResponseHeaders(false) - .handle(new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); - assertThat(requestSnippetLines("request-without-response-header-inclusion"), - hasItem("$ curl http://localhost/foo")); - } - @Test public void requestWithContent() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); @@ -192,67 +177,6 @@ public void requestWithHeaders() throws IOException { hasItem("$ curl http://localhost/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); } - @Test - public void basicResponse() throws IOException { - documentCurlResponse("basic-response").handle( - new StubMvcResult(null, new MockHttpServletResponse())); - assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); - } - - @Test - public void nonOkResponse() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItem("HTTP/1.1 400 Bad Request")); - } - - @Test - public void responseWithHeaders() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); - } - - @Test - public void responseWithHeaderInclusionDisabled() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - response.getWriter().append("content"); - documentCurlResponse("response-with-header-inclusion-disabled") - .includeResponseHeaders(false).handle(new StubMvcResult(null, response)); - List responseSnippetLines = responseSnippetLines("response-with-header-inclusion-disabled"); - assertThat( - responseSnippetLines, - not(hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", - "a: alpha"))); - assertThat(responseSnippetLines, hasItem("content")); - } - - @Test - public void responseWithContent() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("content"); - documentCurlResponse("response-with-content").handle( - new StubMvcResult(null, response)); - assertThat(responseSnippetLines("response-with-content"), - hasItems("HTTP/1.1 200 OK", "content")); - } - - @Test - public void requestAndResponse() throws IOException { - documentCurlRequestAndResponse("request-and-response").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), - new MockHttpServletResponse())); - assertThat(requestResponseSnippetLines("request-and-response"), - hasItems("$ curl http://localhost/foo -i", "HTTP/1.1 200 OK")); - } - @Test public void httpWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); @@ -296,16 +220,7 @@ public void requestWithCustomHost() throws IOException { } private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "request"); - } - - private List responseSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "response"); - } - - private List requestResponseSnippetLines(String snippetName) - throws IOException { - return snippetLines(snippetName, "request-response"); + return snippetLines(snippetName, "curl-request"); } private List snippetLines(String snippetName, String snippetType) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java new file mode 100644 index 000000000..1d2546581 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -0,0 +1,175 @@ +/* + * 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.http; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.StubMvcResult; + +/** + * Tests for {@link HttpDocumentation} + * + * @author Andy Wilkinson + */ +public class HttpDocumentationTests { + + private final File outputDir = new File("build/http-documentation-tests"); + + @Before + public void setup() { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + } + + @After + public void cleanup() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void getRequest() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addHeader("Alpha", "a"); + documentHttpRequest("get-request").handle(new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request"), + hasItems("GET /foo HTTP/1.1", "Alpha: a")); + } + + @Test + public void getRequestWithQueryString() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo?bar=baz"); + documentHttpRequest("get-request-with-query-string").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request-with-query-string"), + hasItems("GET /foo?bar=baz HTTP/1.1")); + } + + @Test + public void getRequestWithParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addParameter("b&r", "baz"); + documentHttpRequest("get-request-with-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request-with-parameter"), + hasItems("GET /foo?b%26r=baz HTTP/1.1")); + } + + @Test + public void postRequestWithContent() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + byte[] content = "Hello, world".getBytes(); + request.setContent(content); + documentHttpRequest("post-request-with-content").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("post-request-with-content"), + hasItems("POST /foo HTTP/1.1", "Hello, world")); + } + + @Test + public void postRequestWithParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + request.addParameter("b&r", "baz"); + request.addParameter("a", "alpha"); + documentHttpRequest("post-request-with-parameter").handle( + new StubMvcResult(request, null)); + assertThat( + requestSnippetLines("post-request-with-parameter"), + hasItems("POST /foo HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "b%26r=baz&a=alpha")); + } + + @Test + public void basicResponse() throws IOException { + documentHttpResponse("basic-response").handle( + new StubMvcResult(null, new MockHttpServletResponse())); + assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); + } + + @Test + public void nonOkResponse() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItem("HTTP/1.1 400 Bad Request")); + } + + @Test + public void responseWithHeaders() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setHeader("a", "alpha"); + documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); + } + + @Test + public void responseWithContent() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("content"); + documentHttpResponse("response-with-content").handle( + new StubMvcResult(null, response)); + assertThat(responseSnippetLines("response-with-content"), + hasItems("HTTP/1.1 200 OK", "content")); + } + + private List requestSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "http-request"); + } + + private List responseSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "http-response"); + } + + private List snippetLines(String snippetName, String snippetType) + throws IOException { + File snippetDir = new File(this.outputDir, snippetName); + File snippetFile = new File(snippetDir, snippetType + ".asciidoc"); + String line = null; + List lines = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); + try { + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + finally { + reader.close(); + } + return lines; + } +} From f93981fce244e53fbdfa8945ce6a186a5c1a194a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Apr 2015 12:31:29 +0100 Subject: [PATCH 0044/1059] Align with the Asciidoctor team's preferred file extension, .adoc Closes gh-45 --- README.md | 10 +-- .../src/main/asciidoc/api-guide.adoc} | 63 ++++++++++--------- ...de.asciidoc => getting-started-guide.adoc} | 46 +++++++------- .../src/main/asciidoc/api-guide.adoc} | 63 +++++++++---------- ...de.asciidoc => getting-started-guide.adoc} | 46 +++++++------- .../snippet/SnippetWritingResultHandler.java | 2 +- .../RestDocumentationIntegrationTests.java | 21 +++---- .../restdocs/curl/CurlDocumentationTests.java | 2 +- .../restdocs/http/HttpDocumentationTests.java | 2 +- 9 files changed, 125 insertions(+), 130 deletions(-) rename samples/{rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc => rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc} (67%) rename samples/rest-notes-spring-data-rest/src/main/asciidoc/{getting-started-guide.asciidoc => getting-started-guide.adoc} (76%) rename samples/{rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc => rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc} (67%) rename samples/rest-notes-spring-hateoas/src/main/asciidoc/{getting-started-guide.asciidoc => getting-started-guide.adoc} (76%) diff --git a/README.md b/README.md index 1bc9b39d2..9f8ca3f5b 100644 --- a/README.md +++ b/README.md @@ -279,9 +279,9 @@ command for the request and the resulting response to files in a directory named `index` in the project's `build/generated-snippets/` directory. Three files will be written: - - `index/curl-request.asciidoc` - - `index/http-request.asciidoc` - - `index/http-response.asciidoc` + - `index/curl-request.adoc` + - `index/http-request.adoc` + - `index/http-response.adoc` #### Parameterized output directories @@ -340,8 +340,8 @@ which the snippets are written. For example, to include both the request and res snippets described above: ``` -include::{generated}/index/curl-request.asciidoc[] -include::{generated}/index/http-response.asciidoc[] +include::{generated}/index/curl-request.adoc[] +include::{generated}/index/http-response.adoc[] ``` ## Generating snippets in your IDE diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc similarity index 67% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc rename to samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 3919f86ce..cffe53397 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -64,12 +64,12 @@ use of HTTP status codes. 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 structure: -include::{generated}/error-example/response-fields.asciidoc +include::{generated}/error-example/response-fields.adoc[] For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/http-response.asciidoc[] +include::{generated}/error-example/http-response.adoc[] [[overview-hypermedia]] == Hypermedia @@ -99,18 +99,18 @@ A `GET` request is used to access the index ==== Response structure -include::{generated}/index-example/response-fields.asciidoc[] +include::{generated}/index-example/response-fields.adoc[] ==== Example response -include::{generated}/index-example/http-response.asciidoc[] +include::{generated}/index-example/http-response.adoc[] [[resources-index-links]] ==== Links -include::{generated}/index-example/links.asciidoc[] +include::{generated}/index-example/links.adoc[] @@ -128,15 +128,15 @@ A `GET` request will list all of the service's notes. ==== Response structure -include::{generated}/notes-list-example/response-fields.asciidoc[] +include::{generated}/notes-list-example/response-fields.adoc[] ==== Example request -include::{generated}/notes-list-example/curl-request.asciidoc[] +include::{generated}/notes-list-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-list-example/http-response.asciidoc[] +include::{generated}/notes-list-example/http-response.adoc[] @@ -147,15 +147,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/notes-create-example/request-fields.asciidoc[] +include::{generated}/notes-create-example/request-fields.adoc[] ==== Example request -include::{generated}/notes-create-example/curl-request.asciidoc[] +include::{generated}/notes-create-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-create-example/http-response.asciidoc[] +include::{generated}/notes-create-example/http-response.adoc[] @@ -173,15 +173,15 @@ A `GET` request will list all of the service's tags. ==== Response structure -include::{generated}/tags-list-example/response-fields.asciidoc[] +include::{generated}/tags-list-example/response-fields.adoc[] ==== Example request -include::{generated}/tags-list-example/curl-request.asciidoc[] +include::{generated}/tags-list-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-list-example/http-response.asciidoc[] +include::{generated}/tags-list-example/http-response.adoc[] @@ -192,15 +192,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/tags-create-example/request-fields.asciidoc[] +include::{generated}/tags-create-example/request-fields.adoc[] ==== Example request -include::{generated}/tags-create-example/curl-request.asciidoc[] +include::{generated}/tags-create-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-create-example/http-response.asciidoc[] +include::{generated}/tags-create-example/http-response.adoc[] @@ -214,7 +214,7 @@ The Note resource is used to retrieve, update, and delete individual notes [[resources-note-links]] === Links -include::{generated}/note-get-example/links.asciidoc[] +include::{generated}/note-get-example/links.adoc[] @@ -225,15 +225,15 @@ A `GET` request will retrieve the details of a note ==== Response structure -include::{generated}/note-get-example/response-fields.asciidoc[] +include::{generated}/note-get-example/response-fields.adoc[] ==== Example request -include::{generated}/note-get-example/curl-request.asciidoc[] +include::{generated}/note-get-example/curl-request.adoc[] ==== Example response -include::{generated}/note-get-example/http-response.asciidoc[] +include::{generated}/note-get-example/http-response.adoc[] @@ -244,17 +244,18 @@ A `PATCH` request is used to update a note ==== Request structure -include::{generated}/note-update-example/request-fields.asciidoc[] +include::{generated}/note-update-example/request-fields.adoc[] To leave an attribute of a note unchanged, any of the above may be omitted from the request. ==== Example request -include::{generated}/note-update-example/curl-request.asciidoc[] +include::{generated}/note-update-example/curl-request.adoc[] ==== Example response -include::{generated}/note-update-example/http-response.asciidoc[] +include::{generated}/note-update-example/http-response.adoc[] + [[resources-tag]] @@ -267,7 +268,7 @@ The Tag resource is used to retrieve, update, and delete individual tags [[resources-tag-links]] === Links -include::{generated}/tag-get-example/links.asciidoc[] +include:{{generated}/tag-get-example/links.adoc @@ -278,15 +279,15 @@ A `GET` request will retrieve the details of a tag ==== Response structure -include::{generated}/tag-get-example/response-fields.asciidoc[] +include::{generated}/tag-get-example/response-fields.adoc[] ==== Example request -include::{generated}/tag-get-example/curl-request.asciidoc[] +include::{generated}/tag-get-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-get-example/http-response.asciidoc[] +include::{generated}/tag-get-example/http-response.adoc[] @@ -297,12 +298,12 @@ A `PATCH` request is used to update a tag ==== Request structure -include::{generated}/tag-update-example/request-fields.asciidoc[] +include::{generated}/tag-update-example/request-fields.adoc[] ==== Example request -include::{generated}/tag-update-example/curl-request.asciidoc[] +include::{generated}/tag-update-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-update-example/http-response.asciidoc[] +include::{generated}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc similarity index 76% rename from samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc rename to samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc index c9eac9528..f9d9788c0 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc @@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/curl-request.asciidoc[] +include::{generated}/index/1/curl-request.adoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] To create a note, you need to execute a `POST` request to this URI including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/curl-request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/http-response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.adoc[] To work with the newly created note you use the URI in the `Location` header. For example, you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/curl-request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.adoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/http-response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.adoc[] Note the `tags` link which we'll make use of later. @@ -92,26 +92,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/curl-request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/http-response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.adoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/curl-request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.adoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/http-response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.adoc[] @@ -132,25 +132,25 @@ with it. Once again we execute a `POST` request. However, this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/curl-request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.adoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/creating-a-note/5/http-response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.adoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/curl-request.asciidoc[] -include::{generated}/creating-a-note/6/http-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.adoc[] +include::{generated}/creating-a-note/6/http-response.adoc[] To verify that the tag has been associated with the note, we can perform a `GET` request against the URI from the `tags` link: -include::{generated}/creating-a-note/7/curl-request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.adoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/creating-a-note/7/http-response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.adoc[] @@ -160,18 +160,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll used the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/curl-request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.adoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/http-response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.adoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/creating-a-note/2/http-response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.adoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/curl-request.asciidoc[] -include::{generated}/creating-a-note/9/http-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.adoc[] +include::{generated}/creating-a-note/9/http-response.adoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc similarity index 67% rename from samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc rename to samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 48a32aad6..0a1855b07 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -64,12 +64,12 @@ use of HTTP status codes. 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 structure: -include::{generated}/error-example/response-fields.asciidoc[] +include::{generated}/error-example/response-fields.adoc For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/http-response.asciidoc[] +include::{generated}/error-example/http-response.adoc[] [[overview-hypermedia]] == Hypermedia @@ -99,18 +99,18 @@ A `GET` request is used to access the index ==== Response structure -include::{generated}/index-example/response-fields.asciidoc[] +include::{generated}/index-example/response-fields.adoc[] ==== Example response -include::{generated}/index-example/http-response.asciidoc[] +include::{generated}/index-example/http-response.adoc[] [[resources-index-links]] ==== Links -include::{generated}/index-example/links.asciidoc[] +include::{generated}/index-example/links.adoc[] @@ -128,15 +128,15 @@ A `GET` request will list all of the service's notes. ==== Response structure -include::{generated}/notes-list-example/response-fields.asciidoc[] +include::{generated}/notes-list-example/response-fields.adoc[] ==== Example request -include::{generated}/notes-list-example/curl-request.asciidoc[] +include::{generated}/notes-list-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-list-example/http-response.asciidoc[] +include::{generated}/notes-list-example/http-response.adoc[] @@ -147,15 +147,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/notes-create-example/request-fields.asciidoc[] +include::{generated}/notes-create-example/request-fields.adoc[] ==== Example request -include::{generated}/notes-create-example/curl-request.asciidoc[] +include::{generated}/notes-create-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-create-example/http-response.asciidoc[] +include::{generated}/notes-create-example/http-response.adoc[] @@ -173,15 +173,15 @@ A `GET` request will list all of the service's tags. ==== Response structure -include::{generated}/tags-list-example/response-fields.asciidoc[] +include::{generated}/tags-list-example/response-fields.adoc[] ==== Example request -include::{generated}/tags-list-example/curl-request.asciidoc[] +include::{generated}/tags-list-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-list-example/http-response.asciidoc[] +include::{generated}/tags-list-example/http-response.adoc[] @@ -192,15 +192,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/tags-create-example/request-fields.asciidoc[] +include::{generated}/tags-create-example/request-fields.adoc[] ==== Example request -include::{generated}/tags-create-example/curl-request.asciidoc[] +include::{generated}/tags-create-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-create-example/http-response.asciidoc[] +include::{generated}/tags-create-example/http-response.adoc[] @@ -214,7 +214,7 @@ The Note resource is used to retrieve, update, and delete individual notes [[resources-note-links]] === Links -include::{generated}/note-get-example/links.asciidoc[] +include::{generated}/note-get-example/links.adoc[] @@ -225,15 +225,15 @@ A `GET` request will retrieve the details of a note ==== Response structure -include::{generated}/note-get-example/response-fields.asciidoc[] +include::{generated}/note-get-example/response-fields.adoc[] ==== Example request -include::{generated}/note-get-example/curl-request.asciidoc[] +include::{generated}/note-get-example/curl-request.adoc[] ==== Example response -include::{generated}/note-get-example/http-response.asciidoc[] +include::{generated}/note-get-example/http-response.adoc[] @@ -244,18 +244,17 @@ A `PATCH` request is used to update a note ==== Request structure -include::{generated}/note-update-example/request-fields.asciidoc[] +include::{generated}/note-update-example/request-fields.adoc[] To leave an attribute of a note unchanged, any of the above may be omitted from the request. ==== Example request -include::{generated}/note-update-example/curl-request.asciidoc[] +include::{generated}/note-update-example/curl-request.adoc[] ==== Example response -include::{generated}/note-update-example/http-response.asciidoc[] - +include::{generated}/note-update-example/http-response.adoc[] [[resources-tag]] @@ -268,7 +267,7 @@ The Tag resource is used to retrieve, update, and delete individual tags [[resources-tag-links]] === Links -include:{{generated}/tag-get-example/links.asciidoc +include::{generated}/tag-get-example/links.adoc[] @@ -279,15 +278,15 @@ A `GET` request will retrieve the details of a tag ==== Response structure -include::{generated}/tag-get-example/response-fields.asciidoc[] +include::{generated}/tag-get-example/response-fields.adoc[] ==== Example request -include::{generated}/tag-get-example/curl-request.asciidoc[] +include::{generated}/tag-get-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-get-example/http-response.asciidoc[] +include::{generated}/tag-get-example/http-response.adoc[] @@ -298,12 +297,12 @@ A `PATCH` request is used to update a tag ==== Request structure -include::{generated}/tag-update-example/request-fields.asciidoc[] +include::{generated}/tag-update-example/request-fields.adoc[] ==== Example request -include::{generated}/tag-update-example/curl-request.asciidoc[] +include::{generated}/tag-update-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-update-example/http-response.asciidoc[] +include::{generated}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc similarity index 76% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc rename to samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc index 9134e8521..93b37296c 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/curl-request.asciidoc[] +include::{generated}/index/1/curl-request.adoc[] This request should yield the following response: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] To create a note you need to execute a `POST` request to this URI, including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/curl-request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/http-response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.adoc[] To work with the newly created note you use the URI in the `Location` header. For example you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/curl-request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.adoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/http-response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.adoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/http-response.asciidoc[] +include::{generated}/index/1/http-response.adoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/curl-request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/http-response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.adoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/curl-request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.adoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/http-response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.adoc[] @@ -131,25 +131,25 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/curl-request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.adoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/creating-a-note/5/http-response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.adoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/curl-request.asciidoc[] -include::{generated}/creating-a-note/6/http-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.adoc[] +include::{generated}/creating-a-note/6/http-response.adoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/creating-a-note/7/curl-request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.adoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/creating-a-note/7/http-response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.adoc[] @@ -159,18 +159,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/curl-request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.adoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/http-response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.adoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/creating-a-note/2/http-response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.adoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/curl-request.asciidoc[] -include::{generated}/creating-a-note/9/http-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.adoc[] +include::{generated}/creating-a-note/9/http-response.adoc[] diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 5df711765..819ec6c42 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -57,7 +57,7 @@ public void handle(MvcResult result) throws IOException { private Writer createWriter() throws IOException { File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName - + ".asciidoc"); + + ".adoc"); if (outputFile != null) { File parent = outputFile.getParentFile(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index b52a6ff1e..0f51b4834 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -84,8 +84,7 @@ public void basicSnippetGeneration() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic")); assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), - "http-request.asciidoc", "http-response.asciidoc", - "curl-request.asciidoc"); + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test @@ -97,8 +96,7 @@ public void parameterizedOutputDirectory() throws Exception { .andExpect(status().isOk()).andDo(document("{method-name}")); assertExpectedSnippetFilesExist(new File( "build/generated-snippets/parameterized-output-directory"), - "http-request.asciidoc", "http-response.asciidoc", - "curl-request.asciidoc"); + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test @@ -110,23 +108,20 @@ public void multiStep() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-1/"), - "http-request.asciidoc", "http-response.asciidoc", - "curl-request.asciidoc"); + new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-2/"), - "http-request.asciidoc", "http-response.asciidoc", - "curl-request.asciidoc"); + new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-3/"), - "http-request.asciidoc", "http-response.asciidoc", - "curl-request.asciidoc"); + new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 24131f713..277603c6f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -226,7 +226,7 @@ private List requestSnippetLines(String snippetName) throws IOException private List snippetLines(String snippetName, String snippetType) throws IOException { File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".asciidoc"); + File snippetFile = new File(snippetDir, snippetType + ".adoc"); String line = null; List lines = new ArrayList(); BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index 1d2546581..d0fc8ec20 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -158,7 +158,7 @@ private List responseSnippetLines(String snippetName) throws IOException private List snippetLines(String snippetName, String snippetType) throws IOException { File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".asciidoc"); + File snippetFile = new File(snippetDir, snippetType + ".adoc"); String line = null; List lines = new ArrayList(); BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); From bcda6fe45968d2de7fb9059a6545f0dbd440d958 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Apr 2015 12:34:28 +0100 Subject: [PATCH 0045/1059] Update the main project and Gradle samples to use Gradle 2.3 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 51010 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../rest-notes-spring-data-rest/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../rest-notes-spring-hateoas/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 10159e0cc..3fa5f9aac 100644 --- a/build.gradle +++ b/build.gradle @@ -118,5 +118,5 @@ task buildRestNotesSpringDataRestSampleWithMaven(type: Exec) { } wrapper { - gradleVersion = '2.2' + gradleVersion = '2.3' } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..2322723c7ed5f591adfa4c87b6c51932f591249a 100644 GIT binary patch delta 4145 zcmY+Hc|4SD7stmiC}TG$GnR-Uva772MzZgs#TI3RvCA-7%U*7h$5P1@A!}lcCX9V4 z>9N&gElPITQhI0ZdEe*#n19Z>zt``ab1mmyrYnuPC5@TW(uA4iAOynB4p9m6zQ-xe zPBUlUkKgybr*(mUHDEP>39?`EOoExN>_LNOQ;BW>S*9tvt>J_QLqQPFG6w=bSP=+n z1ljpnK_bsp%pk)&N-$n}wf=zbNeDIw#Fq;Kk8bX8v zCzAq4UdFn~q{@#98rz_a`B(lvQ(o~?&U~yMi61wtJZL_({Z8FZ^1hV{0X{87Gq-4|)(GlH{a8=GS@M+1% z*t31OAfx7cCctk)$<}pMn^s^wyTj=A+_R@Lf$rG?QW)QBCtmtksd9|U9#-O*LoA?b zO*{TJ4YfaZKM1xfE#2`DDVjH~`Le zonX6F7W3{=*0Zra-qsHt=X*FvI^_xi!K;dsUg{t323H;m$c7g#A*&V)hC;KNu40cJ zBjxA6`$UYwyfih-P2(yGAqo2Ja*!HT#nwx1))lOJVIuxPXZn~0y$z+55A*vO*MIiG zH6mcaeIJ60#p@oO`uRXQG3CqkA)#EK9yfw;9VMw->>}(v!u@P>*nIil5kk135`3+P zkRrC9@96BiY|$4p#HFdG9@5M;SY#*qYg*}}R)>HeZ5cP}v7-Zv1RFha+yC3Hn)uZtPZvjyBr)mxcVNc(zJl?O~`QXkLlh$^&NgUmy}D$ z&*@#{t1b#`Kkj=k%qF+p!7&7eJB)IPCohC96{O1T8d_V5=2f5HEIgj;@so>dqw4@d z$lt0hTwAm)-4;I6Ay?3|45B^7R*7 z&kIv67bQ{3u}w_6lK2BlE}ZOb^r|O}Y?}#C?3AOQT_6GGn^Gpj7tboc_BXr|^kh3f zX<~+(y#2&59kXhLAhbNRE1R)H5Sp(bYQv1~e><+_A`tMjRET8*TjC z(W7#b)&-SI?iVIDy>=hj$;|>!zes1F`&8umBJ)#`MZRv;C07jkQ|pB7r;daq0>uQg zl-xKw##%DXNj45($xj!mp?VHypIAVP7H{7c_zK-K_C2WDf7H0CXiCatmg8w~vP*_v zwj1o(_>|dEl5MDhI`>d*JyW{BLJ3Kr!meQD8j9D!^RoW37HN`+Qgo?d$ktz?(VOIl zGOXEyxwWn-vvPX23N*_>)Exfn@VBY3kcyuTuU z{p_>$anG2Fg|flFX6AdM_+)&N1^kCYm{gKyevNB(`!!-}r+_RUpDzFwWA3JX;u}T}Vw4sa+R#LH^_@?!uKH$zL0&pBv9c<~5P=jmK^~w)KM73D zDQU1={-9I)VmNb2%_OAVFl*Do@3H2rgr&Cein^t0G>+RYmh2Th(WosWv&(m5Yvmmp z7}6aYbjEbBruD_&`9jb^Nab2&;_#6{%$EWa`O5~n*b2VP$cN0Om2CtfPxC9Q0h;;p zAwoS0+x;r9*~gR`u<)e^A9prx z-eC7qoa*)iBNad+#J0QO2Z8ug&u5%TAJ7^?) z`%(BstMEuF#WsH8Wb~ixaB~%vOk*DXwsNv&|kXV;@)>_fo)SuLMiTL-I#N2 z_#N!`v1w-$eLfb6+IT@RJlZrv!kje93794SIp1u#@>uVUn4Pw2!uw(#{76pV?(x8p z9*PhFLT)QP)$3Bu&TV1#qZj!$lKjI;di#cYXS(e}rHQXe50BViAXo71M16XG%xq=N zUA=gLN8@nL5*4K|mY71gP8e7=K@oU&R>xh$Bl^yGw}*?_K3_|ZYnuPDYCO>8vwnAS zsJc!{Ud>ick2AuogmQqDuLU#2*Y)^a_ve{HYzm*?jf(!wm)=}~S;V+WaWmqJqcOyA zkI?5YyA}ANoJa2M{5jfKU>&?ZyOgBe;jDg0=1R?D->&acc|4;U^$@~YZFU|Poi|8N z=IsX?Rr+C}g!F}WC*w0?iFzj*tF!nI+SPQgscDq0Kb%_CvpIcD+h5?Y;pq|&`7tg( zKJJ~oxdkm`JzmksS<>={i20A3aRxs;S+-KXa|z1d&avwKaPF9`;GS@X;#6Gl@>_!%dyP-+U1^jMz%(7k0L>W+d0Sf#)E^)$T1#mS7*pX(M-Asfi~3m6`af!@ZG^qzG+R?3Xpl1afgUq4FOs zbK6n$-%vWhP;3T%|< zGonwuc*h{{N?V4;y&7QVr02^{)M>}idf*JeT&KIQ2dY(+?dd&WS0_V%6aa)ewST|y zfyO#b5N_8gf>!R8258-1>4LWCl{siPUipF6tKJN>uj(B^%hzxjw1Ewb>;493uqFTf zqz5!eMxf#rNd#(-0+`GYKnjWzppf7o?x*26c{}xqX-mG28EVfY`{BZF2KK$ zAtum_y1Bv80j!=9|zzN$PwBe{O?yAoqz$wWDGYTOlF9|=N=OT zV$TeLocy1XW^1D-O=)6HAd1W=mP!_Z&YJ_xuTVe-nNMurjYiW;?u&H;w#h1D^n(Cz zG1}N6S`FcSnsvaqNe0x=G41TZANPMP1i}OPt-l7OH;K|a1*mKiVUhrPn@)pQ+^1RTw0j)w5kI8J2T!dYgFBr?xL$3?SZi z63pb>CIX!<0T-Ct#^@dZqLoA6mS426Q~ScCfvL80V3>Tn2y~|c4AZUTrB?)T3JqIU j1In#v;IC=~P+ZTL$iEK{S~jtL8D|0B4i(nxb#(Y2rs?Nw delta 4344 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 Ph(+00+SaCC8;kxAYaCJ{ delta 1783 zcmZXU3rtg29L8^7wDKs0L2;do4tYc#qm(hU!}ttQ-j7ikR0C3A2!V=>Ido_>_*jIE z9-@?CmdH@)7KXIEN};V#kOU$mEM*dgC>vikUuG#r57ojx7~Zm)%w2QWvFesfb2wg6;~L6g=dM#=nE8 z5%|#_Q$ow77t@h-iTOLS6s&q=Y27%^HpmO)-of5DFNlRaJANt~sa$tIN2G29*x~B! z;Bs5!1%+p0rH+u|ycf|doVOBN%;u0i{^{6I0!~X0IB|ExBrp06Cx}EJi<))f;C2w( z#s^QN!Yz*=#(Zw|9s+hpwx0`)+gH#lH3Z zk{EHjdQv?)?j5~qI2dL5zK%10pySe^){MG}nf)h+Sys)&4~{)FS!J*;t}s7t?lmph z_s!!`ki@oh)$g=z>2_Wf>Z-dhrZoj-csGeZ_uPA0mR8uQrrn8D47eH_ z$G!nh(IKn7-ZQ0THOBp^WnoJ_3u^gU>$Ay2S0fdo8)tp3Lt5giGY%IU5??J8a@%Kp z(!b2EYgYY|`0Gm6c=ew3_eTCnV3Xp{B!xCKk)>@qHiuPhhKh!& zqRe+WPUO^WdHj3Bbb^?$A&4-bEbszT$812PiKkWPS@E>u0ttgl!k(prsVWvhs2#w$ zav^Xu+iT}e*0E6JmCs+{@bWKCu#xxT{0R02E@*JtXXVw-sOnI&h&|f3Q&`R1Y-OVA z&0BN^)nc2e`BV*TQ3G4t{a=eyeSGa=!!11R`dt~m(aXlYI6%_$5PJ^INtlzjs2Cjb zuhfnudQ|=}08_izgY5I`w7)9(Fj4`Vq?*Q@PPqyh4itPt1^m zPFcbPv=~>Q?s$g+nT@5BG8mWu0|PgJ=`0F-*5N~e+nyYqu)vkF3?D(r29V96KnX_( zQabD@IP)i^J{_j=;0xZE>dvL$c&-2#JMGacdDtnSKj4wRO1}eHfqDV`wJWqdL7v_j zc}nPg;!aJ!=I#KD`nBoMzZ^hK@A5&YUzdP>J(xn{xoj{J%ms=rjGu=!k8rA%814oh zbfK32ICEMrfbEXjZ+>D|BPiGq;Rv=G5Ilcd8m6TYgp@%LJ2yOxr_m6WfHDKVTctrj zpN|FRc6Tsfa9;o5O&j<$IrzsQ_8`|T^>KqaZ^O*38}eR)!`;5r_5sD+yO6loorVOj zClQJ4o@^vWdUBBX$cUL*BX(G(3{*N{%0n}?Gj@J6%aIdvUx9@AJ}$ZT v9z{;cny~g}A!l!&6p7ltwVPU!kR!9;4*M;c$O-J1AyLtvwZ4Iu`{VuvJJ`1a diff --git a/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties index 56b98e709..72d2eb271 100644 --- a/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Sep 25 10:09:28 BST 2014 +#Thu Apr 16 12:33:42 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 6f324f815..fe5b449c9 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -61,7 +61,7 @@ jar { } task wrapper(type: Wrapper) { - gradleVersion = '2.1' + gradleVersion = '2.3' } eclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index b7612167031001b7b84baf2a959e8ea8ad03c011..3d0dee6e8edfecc92e04653ec780de06f7b34f8b 100644 GIT binary patch delta 1824 zcmZXU3rtg29L8^NTWGbE%A!{!C?1bGl(y%cN}&s%Inz!4d&4S2<>CjnkJXRVENUBeS;|59q$@uE z?TOA{4{HetjeyRjg$(lB!}y$5ilXKz&=4U3bv;7R#p%RQCbyBs%9>|kf|a}Q3h{1> zJh|B8ixW(-O1F12$EteYPEu|1F4=)Sk5Cm+-$cks-lHR2lD8I>YszCHyGKZz2g@S6g zW&NG=Ma_z4x5%q)v$i*dm8_lZv*Xu(YSR7rVy;XPzg}?8+4_Ro$$#NYg{Z~g_NwZ2$1vLT343cQnMVXB@$MW(X!ggK z(ntY)!ZNmgP;6jJ)23V|sxG;Tg{xjE4!m8=P`eEA^(h+8u?a;5fTGOZU{PrT4(mml z+Kf3eEH^WlEky71)I>rRm!fLT!08exFgFM^%h}&@apalL{w3ih1t(3h_oQ%!c;m&b zB<<6Z^A@;jRPT<}x2_7KDbais-M`^DxOweYk zC3AreCiMZRj$pCdQp$8&p?F#@hQV2|)whi} ziUx*m2jJD?hrq4tLC1V;%aV6az!XJ@)sLQjeq zze>9oTOL{o{n8qtO06UIilC>kN8XqAW?)deIUM?D_o3l^Z(`J~PeRvtBQ#2A3T}D} zK~^8Z3t{bzP*jtJz6O5m!!56Tp0Aa_c5ex`zqNA_2-ZfJ1Dk#fKfEjt(6A^<&Zek9 zqnC022!fSbGob3eK%D!ARO}ob%EH1hL`v4fN!ZET6m!FA*a;tzV^KG?Dqo4#!4Qu;Ve7 Ph(+00+SaCC8;kxAYaCJ{ delta 1783 zcmZXU3rtg29L8^7wDKs0L2;do4tYc#qm(hU!}ttQ-j7ikR0C3A2!V=>Ido_>_*jIE z9-@?CmdH@)7KXIEN};V#kOU$mEM*dgC>vikUuG#r57ojx7~Zm)%w2QWvFesfb2wg6;~L6g=dM#=nE8 z5%|#_Q$ow77t@h-iTOLS6s&q=Y27%^HpmO)-of5DFNlRaJANt~sa$tIN2G29*x~B! z;Bs5!1%+p0rH+u|ycf|doVOBN%;u0i{^{6I0!~X0IB|ExBrp06Cx}EJi<))f;C2w( z#s^QN!Yz*=#(Zw|9s+hpwx0`)+gH#lH3Z zk{EHjdQv?)?j5~qI2dL5zK%10pySe^){MG}nf)h+Sys)&4~{)FS!J*;t}s7t?lmph z_s!!`ki@oh)$g=z>2_Wf>Z-dhrZoj-csGeZ_uPA0mR8uQrrn8D47eH_ z$G!nh(IKn7-ZQ0THOBp^WnoJ_3u^gU>$Ay2S0fdo8)tp3Lt5giGY%IU5??J8a@%Kp z(!b2EYgYY|`0Gm6c=ew3_eTCnV3Xp{B!xCKk)>@qHiuPhhKh!& zqRe+WPUO^WdHj3Bbb^?$A&4-bEbszT$812PiKkWPS@E>u0ttgl!k(prsVWvhs2#w$ zav^Xu+iT}e*0E6JmCs+{@bWKCu#xxT{0R02E@*JtXXVw-sOnI&h&|f3Q&`R1Y-OVA z&0BN^)nc2e`BV*TQ3G4t{a=eyeSGa=!!11R`dt~m(aXlYI6%_$5PJ^INtlzjs2Cjb zuhfnudQ|=}08_izgY5I`w7)9(Fj4`Vq?*Q@PPqyh4itPt1^m zPFcbPv=~>Q?s$g+nT@5BG8mWu0|PgJ=`0F-*5N~e+nyYqu)vkF3?D(r29V96KnX_( zQabD@IP)i^J{_j=;0xZE>dvL$c&-2#JMGacdDtnSKj4wRO1}eHfqDV`wJWqdL7v_j zc}nPg;!aJ!=I#KD`nBoMzZ^hK@A5&YUzdP>J(xn{xoj{J%ms=rjGu=!k8rA%814oh zbfK32ICEMrfbEXjZ+>D|BPiGq;Rv=G5Ilcd8m6TYgp@%LJ2yOxr_m6WfHDKVTctrj zpN|FRc6Tsfa9;o5O&j<$IrzsQ_8`|T^>KqaZ^O*38}eR)!`;5r_5sD+yO6loorVOj zClQJ4o@^vWdUBBX$cUL*BX(G(3{*N{%0n}?Gj@J6%aIdvUx9@AJ}$ZT v9z{;cny~g}A!l!&6p7ltwVPU!kR!9;4*M;c$O-J1AyLtvwZ4Iu`{VuvJJ`1a diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 56b98e709..2b8cbfe8d 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Sep 25 10:09:28 BST 2014 +#Thu Apr 16 12:33:24 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip From a5e26a57d849aee9f02f9ada96501a49e785bfb5 Mon Sep 17 00:00:00 2001 From: Dmitriy Mayboroda Date: Thu, 16 Apr 2015 11:12:00 +0300 Subject: [PATCH 0046/1059] Allow requests to be configured with a custom context path Closes gh-49 Closes gh-27 --- .gitignore | 4 +++- .../config/RestDocumentationConfigurer.java | 24 +++++++++++++++++-- .../restdocs/curl/CurlDocumentation.java | 5 ++++ .../util/DocumentableHttpServletRequest.java | 10 ++++++++ .../RestDocumentationConfigurerTests.java | 12 ++++++++++ .../restdocs/curl/CurlDocumentationTests.java | 12 ++++++++++ 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 91fa34bad..19e5b7d98 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ .settings bin build -target \ No newline at end of file +target +.idea +*.iml \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 57b37f983..73c34f508 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -28,8 +28,8 @@ * A {@link MockMvcConfigurer} that can be used to configure the documentation * * @author Andy Wilkinson + * @author Dmitriy Mayboroda * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) - * */ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { @@ -51,12 +51,20 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { */ public static final int DEFAULT_PORT = 8080; + /** + * The default context path for documented URIs + * @see #withContextPath(String) + */ + public static final String DEFAULT_CONTEXT_PATH = ""; + private String scheme = DEFAULT_SCHEME; private String host = DEFAULT_HOST; private int port = DEFAULT_PORT; + private String contextPath = DEFAULT_CONTEXT_PATH; + /** * Configures any documented URIs to use the given {@code scheme}. The default is * {@code http}. @@ -93,6 +101,18 @@ public RestDocumentationConfigurer withPort(int port) { return this; } + /** + * Configures any documented URIs to use the given {@code contextPath}. The default is + * an empty string. + * + * @param The context path + * @return {@code this} + */ + public RestDocumentationConfigurer withContextPath(String contextPath) { + this.contextPath = contextPath; + return this; + } + @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { @@ -109,6 +129,7 @@ public MockHttpServletRequest postProcessRequest( request.setScheme(RestDocumentationConfigurer.this.scheme); request.setServerPort(RestDocumentationConfigurer.this.port); request.setServerName(RestDocumentationConfigurer.this.host); + request.setContextPath(RestDocumentationConfigurer.this.contextPath); configureContentLengthHeaderIfAppropriate(request); return request; } @@ -124,5 +145,4 @@ private void configureContentLengthHeaderIfAppropriate( }; } - } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 960ba88ca..7a38d8671 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -33,6 +33,7 @@ * * @author Andy Wilkinson * @author Yann Le Guern + * @author Dmitriy Mayboroda */ public abstract class CurlDocumentation { @@ -88,6 +89,10 @@ public void perform() throws IOException { this.writer.print(String.format(":%d", request.getPort())); } + if (StringUtils.hasText(request.getContextPath())) { + this.writer.print(String.format("/%s", request.getContextPath())); + } + this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&")); this.writer.print(" -i"); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java index 03fa98a44..3d228cab3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -186,6 +186,16 @@ public String getParameterMapAsQueryString() { return toQueryString(this.delegate.getParameterMap()); } + /** + * Returns the request's context path + * + * @return The context path of the request + * @see HttpServletRequest#getContextPath() + */ + public String getContextPath() { + return this.delegate.getContextPath(); + } + private String getQueryString() { if (this.delegate.getQueryString() != null) { return this.delegate.getQueryString(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index f5c910a30..729ee018b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -35,6 +35,7 @@ * Tests for {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @author Dmitriy Mayboroda */ public class RestDocumentationConfigurerTests { @@ -76,6 +77,17 @@ public void customPort() { assertUriConfiguration("http", "localhost", 8081); } + @Test + public void customContextPath() { + String contextPath = "context-path"; + RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + .withContextPath(contextPath).beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + + assertUriConfiguration("http", "localhost", 8080); + assertThat(this.request.getContextPath(), equalTo(contextPath)); + } + @Test public void noContentLengthHeaderWhenRequestHasNotContent() { RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 277603c6f..0cebc646d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -39,6 +39,7 @@ * * @author Andy Wilkinson * @author Yann Le Guern + * @author Dmitriy Mayboroda */ public class CurlDocumentationTests { @@ -219,6 +220,17 @@ public void requestWithCustomHost() throws IOException { hasItem("$ curl http://api.example.com/foo -i")); } + @Test + public void requestWithContextPath() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setServerName("api.example.com"); + request.setContextPath("v3"); + documentCurlRequest("request-with-custom-context").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-custom-context"), + hasItem("$ curl http://api.example.com/v3/foo -i")); + } + private List requestSnippetLines(String snippetName) throws IOException { return snippetLines(snippetName, "curl-request"); } From d686908b193d71a50210af8c426c68ded347ccb1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2015 09:36:08 +0100 Subject: [PATCH 0047/1059] Ensure that a context path without a leading slash is handled correctly A context path should either be an empty String, or a String that begins with a slash. This commit updates both CurlDocumentation and RestDocumentationConfigurer to prepend a / to the specified context path when required. See gh-49 --- .../config/RestDocumentationConfigurer.java | 3 ++- .../restdocs/curl/CurlDocumentation.java | 4 +++- .../RestDocumentationConfigurerTests.java | 13 ++++++++++++- .../restdocs/curl/CurlDocumentationTests.java | 17 ++++++++++++++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 73c34f508..858690bdc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -109,7 +109,8 @@ public RestDocumentationConfigurer withPort(int port) { * @return {@code this} */ public RestDocumentationConfigurer withContextPath(String contextPath) { - this.contextPath = contextPath; + this.contextPath = (StringUtils.hasText(contextPath) && !contextPath + .startsWith("/")) ? "/" + contextPath : contextPath; return this; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 7a38d8671..b7eb77337 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -90,7 +90,9 @@ public void perform() throws IOException { } if (StringUtils.hasText(request.getContextPath())) { - this.writer.print(String.format("/%s", request.getContextPath())); + this.writer.print(String.format( + request.getContextPath().startsWith("/") ? "%s" : "/%s", + request.getContextPath())); } this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&")); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 729ee018b..1ec54d306 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -78,12 +78,23 @@ public void customPort() { } @Test - public void customContextPath() { + public void customContextPathWithoutSlash() { String contextPath = "context-path"; RequestPostProcessor postProcessor = new RestDocumentationConfigurer() .withContextPath(contextPath).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); + assertUriConfiguration("http", "localhost", 8080); + assertThat(this.request.getContextPath(), equalTo("/" + contextPath)); + } + + @Test + public void customContextPathWithSlash() { + String contextPath = "/context-path"; + RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + .withContextPath(contextPath).beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + assertUriConfiguration("http", "localhost", 8080); assertThat(this.request.getContextPath(), equalTo(contextPath)); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 0cebc646d..83a7ff4fe 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -221,13 +221,24 @@ public void requestWithCustomHost() throws IOException { } @Test - public void requestWithContextPath() throws IOException { + public void requestWithContextPathWithSlash() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setServerName("api.example.com"); + request.setContextPath("/v3"); + documentCurlRequest("request-with-custom-context-with-slash").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("request-with-custom-context-with-slash"), + hasItem("$ curl http://api.example.com/v3/foo -i")); + } + + @Test + public void requestWithContextPathWithoutSlash() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("v3"); - documentCurlRequest("request-with-custom-context").handle( + documentCurlRequest("request-with-custom-context-without-slash").handle( new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-context"), + assertThat(requestSnippetLines("request-with-custom-context-without-slash"), hasItem("$ curl http://api.example.com/v3/foo -i")); } From 4cb5f228a9e588da0c95149b454a2c6bd14dbdab Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2015 09:48:55 +0100 Subject: [PATCH 0048/1059] Correct the javadoc on RestDocmentationConfigurer.withContextPath --- .../restdocs/config/RestDocumentationConfigurer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 858690bdc..bdb56c938 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -105,7 +105,7 @@ public RestDocumentationConfigurer withPort(int port) { * Configures any documented URIs to use the given {@code contextPath}. The default is * an empty string. * - * @param The context path + * @param contextPath The context path * @return {@code this} */ public RestDocumentationConfigurer withContextPath(String contextPath) { From 18c7799b8ff5d436f6fd971999a6153ebddd35a1 Mon Sep 17 00:00:00 2001 From: Preben Asmussen Date: Sat, 18 Apr 2015 17:29:53 +0200 Subject: [PATCH 0049/1059] Fix the artifact id in pom.xml of rest-notes-spring-data-rest Closes gh-53 --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 1cc246de8..5346f7699 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.example - rest-notes-spring-hateoas + rest-notes-spring-data-rest 0.0.1-SNAPSHOT jar From 7a8dfeabd7005d9c489d32b69e66bac05848ee9b Mon Sep 17 00:00:00 2001 From: Dewet Diener Date: Sat, 18 Apr 2015 14:30:27 +0100 Subject: [PATCH 0050/1059] Improve and simplify quoting for cURL commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single quotes are generally safer to use since shells won't try to interpolate any strings or do variable substitution inside it. This could be refined to do a check for [&%$] inside the URI/query string before just blindly surrounding with quotes, but this is safe as it is. Also, don't escape POST paylods since they are quoted already – this doesn't work correctly right now. Closes gh-55 --- .../restdocs/curl/CurlDocumentation.java | 11 +++--- .../restdocs/curl/CurlDocumentationTests.java | 36 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index b7eb77337..732c3ad5c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -82,7 +82,7 @@ private static final class CurlRequestDocumentationAction implements public void perform() throws IOException { DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( this.result.getRequest()); - this.writer.print(String.format("curl %s://%s", request.getScheme(), + this.writer.print(String.format("curl '%s://%s", request.getScheme(), request.getHost())); if (isNonStandardPort(request)) { @@ -95,9 +95,9 @@ public void perform() throws IOException { request.getContextPath())); } - this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&")); + this.writer.print(request.getRequestUriWithQueryString()); - this.writer.print(" -i"); + this.writer.print("' -i"); if (!request.isGetRequest()) { this.writer.print(String.format(" -X %s", request.getMethod())); @@ -105,7 +105,7 @@ public void perform() throws IOException { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { - this.writer.print(String.format(" -H \"%s: %s\"", entry.getKey(), + this.writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } @@ -117,8 +117,7 @@ public void perform() throws IOException { else if (request.isPostRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { - this.writer.print(String.format(" -d '%s'", - queryString.replace("&", "\\&"))); + this.writer.print(String.format(" -d '%s'", queryString)); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 83a7ff4fe..296408c5e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -61,7 +61,7 @@ public void getRequest() throws IOException { documentCurlRequest("get-request").handle( new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); assertThat(requestSnippetLines("get-request"), - hasItem("$ curl http://localhost/foo -i")); + hasItem("$ curl 'http://localhost/foo' -i")); } @Test @@ -69,7 +69,7 @@ public void nonGetRequest() throws IOException { documentCurlRequest("non-get-request").handle( new StubMvcResult(new MockHttpServletRequest("POST", "/foo"), null)); assertThat(requestSnippetLines("non-get-request"), - hasItem("$ curl http://localhost/foo -i -X POST")); + hasItem("$ curl 'http://localhost/foo' -i -X POST")); } @Test @@ -79,7 +79,7 @@ public void requestWithContent() throws IOException { documentCurlRequest("request-with-content").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-content"), - hasItem("$ curl http://localhost/foo -i -d 'content'")); + hasItem("$ curl 'http://localhost/foo' -i -d 'content'")); } @Test @@ -88,7 +88,7 @@ public void requestWitUriQueryString() throws IOException { new StubMvcResult(new MockHttpServletRequest("GET", "/foo?param=value"), null)); assertThat(requestSnippetLines("request-with-uri-query-string"), - hasItem("$ curl http://localhost/foo?param=value -i")); + hasItem("$ curl 'http://localhost/foo?param=value' -i")); } @Test @@ -98,7 +98,7 @@ public void requestWithQueryString() throws IOException { documentCurlRequest("request-with-query-string").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-query-string"), - hasItem("$ curl http://localhost/foo?param=value -i")); + hasItem("$ curl 'http://localhost/foo?param=value' -i")); } @Test @@ -108,7 +108,7 @@ public void requestWithOneParameter() throws IOException { documentCurlRequest("request-with-one-parameter").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-one-parameter"), - hasItem("$ curl http://localhost/foo?k1=v1 -i")); + hasItem("$ curl 'http://localhost/foo?k1=v1' -i")); } @Test @@ -120,7 +120,7 @@ public void requestWithMultipleParameters() throws IOException { documentCurlRequest("request-with-multiple-parameters").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-multiple-parameters"), - hasItem("$ curl http://localhost/foo?k1=v1\\&k1=v1-bis\\&k2=v2 -i")); + hasItem("$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); } @Test @@ -130,7 +130,7 @@ public void requestWithUrlEncodedParameter() throws IOException { documentCurlRequest("request-with-url-encoded-parameter").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-url-encoded-parameter"), - hasItem("$ curl http://localhost/foo?k1=foo+bar%26 -i")); + hasItem("$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); } @Test @@ -140,7 +140,7 @@ public void postRequestWithOneParameter() throws IOException { documentCurlRequest("post-request-with-one-parameter").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("post-request-with-one-parameter"), - hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=v1'")); + hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); } @Test @@ -153,7 +153,7 @@ public void postRequestWithMultipleParameters() throws IOException { new StubMvcResult(request, null)); assertThat( requestSnippetLines("post-request-with-multiple-parameters"), - hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=v1\\&k1=v1-bis\\&k2=v2'")); + hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1&k1=v1-bis&k2=v2'")); } @Test @@ -163,7 +163,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { documentCurlRequest("post-request-with-url-encoded-parameter").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("post-request-with-url-encoded-parameter"), - hasItem("$ curl http://localhost/foo -i -X POST -d 'k1=a%26b'")); + hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); } @Test @@ -175,7 +175,7 @@ public void requestWithHeaders() throws IOException { new StubMvcResult(request, null)); assertThat( requestSnippetLines("request-with-headers"), - hasItem("$ curl http://localhost/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); + hasItem("$ curl 'http://localhost/foo' -i -H 'Content-Type: application/json' -H 'a: alpha'")); } @Test @@ -185,7 +185,7 @@ public void httpWithNonStandardPort() throws IOException { documentCurlRequest("http-with-non-standard-port").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("http-with-non-standard-port"), - hasItem("$ curl http://localhost:8080/foo -i")); + hasItem("$ curl 'http://localhost:8080/foo' -i")); } @Test @@ -196,7 +196,7 @@ public void httpsWithStandardPort() throws IOException { documentCurlRequest("https-with-standard-port").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("https-with-standard-port"), - hasItem("$ curl https://localhost/foo -i")); + hasItem("$ curl 'https://localhost/foo' -i")); } @Test @@ -207,7 +207,7 @@ public void httpsWithNonStandardPort() throws IOException { documentCurlRequest("https-with-non-standard-port").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("https-with-non-standard-port"), - hasItem("$ curl https://localhost:8443/foo -i")); + hasItem("$ curl 'https://localhost:8443/foo' -i")); } @Test @@ -217,7 +217,7 @@ public void requestWithCustomHost() throws IOException { documentCurlRequest("request-with-custom-host").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-custom-host"), - hasItem("$ curl http://api.example.com/foo -i")); + hasItem("$ curl 'http://api.example.com/foo' -i")); } @Test @@ -228,7 +228,7 @@ public void requestWithContextPathWithSlash() throws IOException { documentCurlRequest("request-with-custom-context-with-slash").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-custom-context-with-slash"), - hasItem("$ curl http://api.example.com/v3/foo -i")); + hasItem("$ curl 'http://api.example.com/v3/foo' -i")); } @Test @@ -239,7 +239,7 @@ public void requestWithContextPathWithoutSlash() throws IOException { documentCurlRequest("request-with-custom-context-without-slash").handle( new StubMvcResult(request, null)); assertThat(requestSnippetLines("request-with-custom-context-without-slash"), - hasItem("$ curl http://api.example.com/v3/foo -i")); + hasItem("$ curl 'http://api.example.com/v3/foo' -i")); } private List requestSnippetLines(String snippetName) throws IOException { From c496cfc699717d1ddf590123c57edc4c2bc86088 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2015 18:15:39 +0100 Subject: [PATCH 0051/1059] Extract the logic for building the samples into a plugin --- build.gradle | 47 ++++--------------- .../gradle-plugins/samples.properties | 1 + 2 files changed, 10 insertions(+), 38 deletions(-) create mode 100644 buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties diff --git a/build.gradle b/build.gradle index 3fa5f9aac..fa8faa1d8 100644 --- a/build.gradle +++ b/build.gradle @@ -74,47 +74,18 @@ project(':spring-restdocs') { } } -task buildSamples { - description = 'Assembles and tests the sample projects using both Maven and Gradle' - group = 'Build' - dependsOn 'buildMavenSamples' - dependsOn 'buildGradleSamples' -} - -task buildMavenSamples { - dependsOn 'buildRestNotesSpringHateoasSampleWithMaven' - dependsOn 'buildRestNotesSpringDataRestSampleWithMaven' -} +apply plugin: 'samples' -task buildGradleSamples { - dependsOn 'buildRestNotesSpringHateoasSampleWithGradle' - dependsOn 'buildRestNotesSpringDataRestSampleWithGradle' -} - -task buildRestNotesSpringHateoasSampleWithGradle(type: GradleBuild) { - dependsOn 'spring-restdocs:install' - dir = 'samples/rest-notes-spring-hateoas' - tasks = ['clean', 'build'] -} +samples { + dependOn 'spring-restdocs:install' -task buildRestNotesSpringDataRestSampleWithGradle(type: GradleBuild) { - dependsOn 'spring-restdocs:install' - dir = 'samples/rest-notes-spring-data-rest' - tasks = ['clean', 'build'] -} - -task buildRestNotesSpringHateoasSampleWithMaven(type: Exec) { - dependsOn 'spring-restdocs:install' - workingDir 'samples/rest-notes-spring-hateoas' - def suffix = File.separatorChar == '/' ? '' : '.bat' - commandLine "${System.env.MAVEN_HOME}/bin/mvn${suffix}", 'clean', 'package' -} + restNotesSpringHateoas { + workingDir 'samples/rest-notes-spring-hateoas' + } -task buildRestNotesSpringDataRestSampleWithMaven(type: Exec) { - dependsOn 'spring-restdocs:install' - workingDir 'samples/rest-notes-spring-data-rest' - def suffix = File.separatorChar == '/' ? '' : '.bat' - commandLine "${System.env.MAVEN_HOME}/bin/mvn${suffix}", 'clean', 'package' + restNotesSpringDataRest { + workingDir 'samples/rest-notes-spring-data-rest' + } } wrapper { diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties new file mode 100644 index 000000000..ded5899c5 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties @@ -0,0 +1 @@ +implementation-class: org.springframework.restdocs.build.SamplesPlugin \ No newline at end of file From d1f57f530b52ff4a8d8717ad61aec9f111ae5485 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2015 18:15:39 +0100 Subject: [PATCH 0052/1059] Add the files that were accidentally excluded by .gitignore --- .gitignore | 1 + .../build/SampleBuildConfigurer.groovy | 71 +++++++++++++++++++ .../restdocs/build/SamplesExtension.groovy | 49 +++++++++++++ .../restdocs/build/SamplesPlugin.groovy | 31 ++++++++ 4 files changed, 152 insertions(+) create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy diff --git a/.gitignore b/.gitignore index 19e5b7d98..c508c0c30 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .settings bin build +!buildSrc/src/main/groovy/org/springframework/restdocs/build/ target .idea *.iml \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy new file mode 100644 index 000000000..2ff80d71c --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -0,0 +1,71 @@ +/* + * 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.build + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.GradleBuild + +public class SampleBuildConfigurer { + + private final String name + + private String workingDir + + SampleBuildConfigurer(String name) { + this.name = name + } + + void workingDir(String workingDir) { + this.workingDir = workingDir + } + + Task createTask(Project project, Object... dependencies) { + Task mavenBuild = mavenBuild(project, dependencies) + Task gradleBuild = gradleBuild(project, dependencies) + Task sampleBuild = project.tasks.create name + sampleBuild.description = "Builds the ${name} sample" + sampleBuild.group = "Build" + sampleBuild.dependsOn mavenBuild, gradleBuild + return sampleBuild + } + + private Task mavenBuild(Project project, Object... dependencies) { + Task mavenBuild = project.tasks.create("${name}Maven", Exec) + mavenBuild.description = "Builds the ${name} sample with Maven" + mavenBuild.group = "Build" + mavenBuild.workingDir = this.workingDir + String suffix = File.separatorChar == '/' ? '' : '.bat' + mavenBuild.commandLine = [System.env.MAVEN_HOME ? + "${System.env.MAVEN_HOME}/bin/mvn${suffix}" : "mvn${suffix}", + 'clean', 'package'] + mavenBuild.dependsOn dependencies + return mavenBuild + } + + private Task gradleBuild(Project project, Object... dependencies) { + Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) + gradleBuild.description = "Builds the ${name} sample with Gradle" + gradleBuild.group = "Build" + gradleBuild.dir = this.workingDir + gradleBuild.tasks = ['clean', 'build'] + gradleBuild.dependsOn dependencies + return gradleBuild + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy new file mode 100644 index 000000000..c86a3104a --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy @@ -0,0 +1,49 @@ +/* + * 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.build + +import org.gradle.api.Project +import org.gradle.api.Task + +public class SamplesExtension { + + Project Project + + Task buildSamplesTask + + Object[] dependencies = [] + + SamplesExtension(Project project, Task buildSamplesTask) { + this.project = project + this.buildSamplesTask = buildSamplesTask + } + + void dependOn(Object... paths) { + this.dependencies += paths + } + + def methodMissing(String name, args) { + SampleBuildConfigurer configurer = new SampleBuildConfigurer(name) + Closure closure = args[0] + closure.delegate = configurer + closure.resolveStrategy = Closure.DELEGATE_FIRST + closure.call() + Task task = configurer.createTask(this.project, this.dependencies) + this.buildSamplesTask.dependsOn task + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy new file mode 100644 index 000000000..09af86d80 --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy @@ -0,0 +1,31 @@ +/* + * 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.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +public class SamplesPlugin implements Plugin { + + public void apply(Project project) { + def buildSamplesTask = project.tasks.create('buildSamples') + buildSamplesTask.description = 'Builds the configured samples' + buildSamplesTask.group = 'Build' + project.extensions.create('samples', SamplesExtension, project, buildSamplesTask) + } + +} \ No newline at end of file From d8151e7b037eb191d53362cb8db020095aed8daa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2015 11:24:23 +0100 Subject: [PATCH 0053/1059] Add configuration for building on Travis CI This commit adds the necessary configuration to build the project on Travis CI. First, the main project is built using Java 7 then the samples are built using Java 8. Closes gh-58 --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..43bb5feb1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: java +script: + - jdk_switcher use oraclejdk7 + - "./gradlew build" + - jdk_switcher use oraclejdk8 + - "./gradlew buildSamples" \ No newline at end of file From d0f0de537c6e72050cdf4f1565df78567bfba1a9 Mon Sep 17 00:00:00 2001 From: Heiko Scherrer Date: Sun, 19 Apr 2015 21:22:22 +0200 Subject: [PATCH 0054/1059] Ignore parameters when finding a LinkExtractor for a Content-Type Closes gh-57 --- .../restdocs/hypermedia/LinkExtractors.java | 11 ++++----- .../hypermedia/LinkExtractorsTests.java | 23 ++++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java index 4b1551aea..72a8a4a9f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java @@ -24,11 +24,10 @@ import java.util.Map; import java.util.Map.Entry; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - /** * Static factory methods providing a selection of {@link LinkExtractor link extractors} * for use when documentating a hypermedia-based API. @@ -65,15 +64,15 @@ public static LinkExtractor atomLinks() { /** * Returns the {@code LinkExtractor} for the given {@code contentType} or {@code null} * if there is no extractor for the content type. - * - * @param contentType The content type + * + * @param contentType The content type, may include parameters * @return The extractor for the content type, or {@code null} */ public static LinkExtractor extractorForContentType(String contentType) { - if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) { return atomLinks(); } - else if ("application/hal+json".equals(contentType)) { + else if (MediaType.parseMediaType(contentType).isCompatibleWith(new MediaType("application","hal+json"))) { return halLinks(); } return null; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java index 8af0ba5b4..d5a8302e9 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java @@ -16,7 +16,9 @@ package org.springframework.restdocs.hypermedia; +import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import java.io.File; import java.io.FileReader; @@ -33,10 +35,8 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import org.springframework.http.InvalidMediaTypeException; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.hypermedia.Link; -import org.springframework.restdocs.hypermedia.LinkExtractor; -import org.springframework.restdocs.hypermedia.LinkExtractors; import org.springframework.util.FileCopyUtils; /** @@ -62,6 +62,23 @@ public LinkExtractorsTests(LinkExtractor linkExtractor, String linkType) { this.linkType = linkType; } + @Test(expected = InvalidMediaTypeException.class) + public void emptyContentType() { + LinkExtractors.extractorForContentType(null); + } + + @Test + public void combinedContentTypeMatches() { + LinkExtractor linkExtractor = LinkExtractors.extractorForContentType("application/json;charset=UTF-8"); + assertThat(linkExtractor, notNullValue()); + } + + @Test + public void notDefinedMediaTypesMatches() { + LinkExtractor linkExtractor = LinkExtractors.extractorForContentType("application/hal+json;charset=UTF-8"); + assertThat(linkExtractor, notNullValue()); + } + @Test public void singleLink() throws IOException { Map> links = this.linkExtractor From 474796d15e5f6a62bf4ab3d4c23557e11333a911 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Apr 2015 11:00:50 +0100 Subject: [PATCH 0055/1059] Polish changes to Content-Type handling in LinkExtractors - If the request has no Content-Type (null or an empty string), a null extractor is returned rather than an exception being thrown. This is consistent with the old behaviour. - Tests have been reworked a little bit to separate out the new normal unit tests from the existing parameterised tests. Closes gh-56 --- .../restdocs/hypermedia/LinkExtractors.java | 24 ++-- .../LinkExtractorsPayloadTests.java | 124 ++++++++++++++++++ .../hypermedia/LinkExtractorsTests.java | 121 +++-------------- 3 files changed, 161 insertions(+), 108 deletions(-) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java index 72a8a4a9f..806496c5d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java @@ -24,9 +24,11 @@ import java.util.Map; import java.util.Map.Entry; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; /** * Static factory methods providing a selection of {@link LinkExtractor link extractors} @@ -69,11 +71,14 @@ public static LinkExtractor atomLinks() { * @return The extractor for the content type, or {@code null} */ public static LinkExtractor extractorForContentType(String contentType) { - if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) { - return atomLinks(); - } - else if (MediaType.parseMediaType(contentType).isCompatibleWith(new MediaType("application","hal+json"))) { - return halLinks(); + if (StringUtils.hasText(contentType)) { + MediaType mediaType = MediaType.parseMediaType(contentType); + if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) { + return atomLinks(); + } + if (mediaType.isCompatibleWith(HalLinkExtractor.HAL_MEDIA_TYPE)) { + return halLinks(); + } } return null; } @@ -95,7 +100,10 @@ public Map> extractLinks(MockHttpServletResponse response) } @SuppressWarnings("unchecked") - private static class HalLinkExtractor extends JsonContentLinkExtractor { + static class HalLinkExtractor extends JsonContentLinkExtractor { + + private static final MediaType HAL_MEDIA_TYPE = new MediaType("application", + "hal+json"); @Override public Map> extractLinks(Map json) { @@ -140,7 +148,7 @@ private static void maybeAddLink(Link possibleLink, List links) { } @SuppressWarnings("unchecked") - private static class AtomLinkExtractor extends JsonContentLinkExtractor { + static class AtomLinkExtractor extends JsonContentLinkExtractor { @Override public Map> extractLinks(Map json) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java new file mode 100644 index 000000000..eba107de3 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -0,0 +1,124 @@ +/* + * 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.hypermedia; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Parameterized tests for {@link LinkExtractors} with various payloads. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class LinkExtractorsPayloadTests { + + private final LinkExtractor linkExtractor; + + private final String linkType; + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, + new Object[] { LinkExtractors.atomLinks(), "atom" }); + } + + public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { + this.linkExtractor = linkExtractor; + this.linkType = linkType; + } + + @Test + public void singleLink() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("single-link")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + } + + @Test + public void multipleLinksWithDifferentRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-different-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), + new Link("bravo", "http://bravo.example.com")), links); + } + + @Test + public void multipleLinksWithSameRels() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("multiple-links-same-rels")); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), + new Link("alpha", "http://alpha.example.com/two")), links); + } + + @Test + public void noLinks() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("no-links")); + assertLinks(Collections. emptyList(), links); + } + + @Test + public void linksInTheWrongFormat() throws IOException { + Map> links = this.linkExtractor + .extractLinks(createResponse("wrong-format")); + assertLinks(Collections. emptyList(), links); + } + + private void assertLinks(List expectedLinks, Map> actualLinks) { + Map> expectedLinksByRel = new HashMap<>(); + for (Link expectedLink : expectedLinks) { + List expectedlinksWithRel = expectedLinksByRel.get(expectedLink + .getRel()); + if (expectedlinksWithRel == null) { + expectedlinksWithRel = new ArrayList<>(); + expectedLinksByRel.put(expectedLink.getRel(), expectedlinksWithRel); + } + expectedlinksWithRel.add(expectedLink); + } + assertEquals(expectedLinksByRel, actualLinks); + } + + 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/link-payloads/" + this.linkType + "/" + name + + ".json"); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java index d5a8302e9..89cba6f06 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java @@ -16,129 +16,50 @@ package org.springframework.restdocs.hypermedia; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.junit.Assert.assertEquals; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -import org.springframework.http.InvalidMediaTypeException; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.util.FileCopyUtils; +import org.springframework.restdocs.hypermedia.LinkExtractors.AtomLinkExtractor; +import org.springframework.restdocs.hypermedia.LinkExtractors.HalLinkExtractor; /** * Tests for {@link LinkExtractors}. - * + * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) public class LinkExtractorsTests { - private final LinkExtractor linkExtractor; - - private final String linkType; - - @Parameters - public static Collection data() { - return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, - new Object[] { LinkExtractors.atomLinks(), "atom" }); - } - - public LinkExtractorsTests(LinkExtractor linkExtractor, String linkType) { - this.linkExtractor = linkExtractor; - this.linkType = linkType; - } - - @Test(expected = InvalidMediaTypeException.class) - public void emptyContentType() { - LinkExtractors.extractorForContentType(null); - } - @Test - public void combinedContentTypeMatches() { - LinkExtractor linkExtractor = LinkExtractors.extractorForContentType("application/json;charset=UTF-8"); - assertThat(linkExtractor, notNullValue()); + public void nullContentTypeYieldsNullExtractor() { + assertThat(LinkExtractors.extractorForContentType(null), nullValue()); } @Test - public void notDefinedMediaTypesMatches() { - LinkExtractor linkExtractor = LinkExtractors.extractorForContentType("application/hal+json;charset=UTF-8"); - assertThat(linkExtractor, notNullValue()); + public void emptyContentTypeYieldsNullExtractor() { + assertThat(LinkExtractors.extractorForContentType(""), nullValue()); } @Test - public void singleLink() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("single-link")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + public void applicationJsonContentTypeYieldsAtomExtractor() { + LinkExtractor linkExtractor = LinkExtractors + .extractorForContentType("application/json"); + assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class)); } @Test - public void multipleLinksWithDifferentRels() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("multiple-links-different-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), - new Link("bravo", "http://bravo.example.com")), links); + public void applicationHalJsonContentTypeYieldsHalExtractor() { + LinkExtractor linkExtractor = LinkExtractors + .extractorForContentType("application/hal+json"); + assertThat(linkExtractor, instanceOf(HalLinkExtractor.class)); } @Test - public void multipleLinksWithSameRels() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("multiple-links-same-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), - new Link("alpha", "http://alpha.example.com/two")), links); + public void contentTypeWithParameterYieldsExtractor() { + LinkExtractor linkExtractor = LinkExtractors + .extractorForContentType("application/json;foo=bar"); + assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class)); } - @Test - public void noLinks() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("no-links")); - assertLinks(Collections. emptyList(), links); - } - - @Test - public void linksInTheWrongFormat() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("wrong-format")); - assertLinks(Collections. emptyList(), links); - } - - private void assertLinks(List expectedLinks, Map> actualLinks) { - Map> expectedLinksByRel = new HashMap<>(); - for (Link expectedLink : expectedLinks) { - List expectedlinksWithRel = expectedLinksByRel.get(expectedLink - .getRel()); - if (expectedlinksWithRel == null) { - expectedlinksWithRel = new ArrayList<>(); - expectedLinksByRel.put(expectedLink.getRel(), expectedlinksWithRel); - } - expectedlinksWithRel.add(expectedLink); - } - assertEquals(expectedLinksByRel, actualLinks); - } - - 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/link-payloads/" + this.linkType + "/" + name - + ".json"); - } } From 0e597e94989f4c6d670ec88f5f8bb08c51ea9101 Mon Sep 17 00:00:00 2001 From: Esko Luontola Date: Mon, 20 Apr 2015 15:02:28 +0300 Subject: [PATCH 0056/1059] Fix broken include in Spring HATEOAS sample's API guide Closes gh-59 --- .../rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 0a1855b07..71227a702 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -64,7 +64,7 @@ use of HTTP status codes. 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 structure: -include::{generated}/error-example/response-fields.adoc +include::{generated}/error-example/response-fields.adoc[] For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: From 8166c5e3e730164165e87d0beb4d2249723687f0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Apr 2015 11:19:31 +0100 Subject: [PATCH 0057/1059] =?UTF-8?q?Add=20a=20task=20that=20verifies=20th?= =?UTF-8?q?e=20includes=20in=20a=20sample=E2=80=99s=20.adoc=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance buildSamples such that, once a sample has been built, it looks at the generated HTML files and checks that they do not contain any output that's indicative of a malformed include in a source .adoc file. This additional verification found another malformed include which this commit corrects. --- .../build/SampleBuildConfigurer.groovy | 35 ++++++++++++++++++- .../src/main/asciidoc/api-guide.adoc | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 2ff80d71c..7bf9293fa 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -16,6 +16,7 @@ package org.springframework.restdocs.build +import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec @@ -38,10 +39,12 @@ public class SampleBuildConfigurer { Task createTask(Project project, Object... dependencies) { Task mavenBuild = mavenBuild(project, dependencies) Task gradleBuild = gradleBuild(project, dependencies) + Task verifyIncludes = verifyIncludes(project) + verifyIncludes.dependsOn mavenBuild, gradleBuild Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" - sampleBuild.dependsOn mavenBuild, gradleBuild + sampleBuild.dependsOn mavenBuild, gradleBuild, verifyIncludes return sampleBuild } @@ -68,4 +71,34 @@ public class SampleBuildConfigurer { return gradleBuild } + private Task verifyIncludes(Project project) { + Task verifyIncludes = project.tasks.create("${name}VerifyIncludes") + verifyIncludes.description = "Verifies the includes in the ${name} sample" + verifyIncludes << { + Map unprocessedIncludes = [:] + [new File(this.workingDir, "build/asciidoc"), + new File(this.workingDir, "target/generated-docs")].each { buildDir -> + buildDir.eachFileRecurse { file -> + if (file.name.endsWith('.html')) { + file.eachLine { line -> + if (line.contains(new File(this.workingDir).absolutePath)) { + unprocessedIncludes.get(file, []).add(line) + } + } + } + } + } + if (unprocessedIncludes) { + StringWriter message = new StringWriter() + PrintWriter writer = new PrintWriter(message) + writer.println 'Found unprocessed includes:' + unprocessedIncludes.each { file, lines -> + writer.println " ${file}:" + lines.each { line -> writer.println " ${line}" } + } + throw new GradleException(message.toString()) + } + } + return verifyIncludes + } } \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index cffe53397..9b14b377c 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -268,7 +268,7 @@ The Tag resource is used to retrieve, update, and delete individual tags [[resources-tag-links]] === Links -include:{{generated}/tag-get-example/links.adoc +include::{generated}/tag-get-example/links.adoc[] From 764daf7c9911856037f90f389960b3ad65b7d900 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Apr 2015 16:43:50 +0100 Subject: [PATCH 0058/1059] Add support for documenting fields in payloads that contain arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the path used to document a field had to be dot-separated, with each segment of the path being used as a key in a map. This made it impossible to document fields in a payload within an array. This commit adds support for using [] in a field’s path to represent an array. For example, the fields in following JSON payload: { "id": 67, “date”: "2015-01-20", "assets": [ { "id":356, "name": "sample" } ] } Can now be documented using the following paths: - id - date - assets[].id - assets[].name Closes gh-60 --- build.gradle | 3 + .../restdocs/payload/FieldExtractor.java | 52 ---- .../restdocs/payload/FieldPath.java | 98 +++++++ .../restdocs/payload/FieldProcessor.java | 277 ++++++++++++++++++ .../payload/FieldSnippetResultHandler.java | 15 - .../restdocs/payload/FieldType.java | 2 +- .../restdocs/payload/FieldTypeResolver.java | 19 +- .../restdocs/payload/FieldValidator.java | 31 +- .../payload/PayloadDocumentation.java | 50 ++++ .../restdocs/payload/FieldPathTests.java | 61 ++++ .../restdocs/payload/FieldProcessorTests.java | 233 +++++++++++++++ .../payload/FieldTypeResolverTests.java | 14 + .../restdocs/payload/FieldValidatorTests.java | 18 +- .../payload/PayloadDocumentationTests.java | 155 ++++++++++ 14 files changed, 924 insertions(+), 104 deletions(-) delete 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/FieldPath.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java diff --git a/build.gradle b/build.gradle index fa8faa1d8..46195c883 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ project(':spring-restdocs') { ext { + hamcrestVersion = '1.3' jacksonVersion = '2.3.4' jacocoVersion = '0.7.2.201409121644' junitVersion = '4.11' @@ -67,6 +68,8 @@ project(':spring-restdocs') { testCompile "org.springframework:spring-webmvc:$springVersion" testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion" testCompile "org.mockito:mockito-core:$mockitoVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" } test { 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 deleted file mode 100644 index d3363662f..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldExtractor.java +++ /dev/null @@ -1,52 +0,0 @@ -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/FieldPath.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java new file mode 100644 index 000000000..e72c6283b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java @@ -0,0 +1,98 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A path that identifies a field in a payload + * + * @author Andy Wilkinson + * + */ +class FieldPath { + + private static final Pattern ARRAY_INDEX_PATTERN = Pattern + .compile("\\[([0-9]+|\\*){0,1}\\]"); + + private final String rawPath; + + private final List segments; + + private final boolean precise; + + private FieldPath(String rawPath, List segments, boolean precise) { + this.rawPath = rawPath; + this.segments = segments; + this.precise = precise; + } + + boolean isPrecise() { + return this.precise; + } + + List getSegments() { + return this.segments; + } + + @Override + public String toString() { + return this.rawPath; + } + + static FieldPath compile(String path) { + List segments = extractSegments(path); + return new FieldPath(path, segments, matchesSingleValue(segments)); + } + + static boolean isArraySegment(String segment) { + return ARRAY_INDEX_PATTERN.matcher(segment).matches(); + } + + static boolean matchesSingleValue(List segments) { + for (String segment : segments) { + if (isArraySegment(segment)) { + return false; + } + } + return true; + } + + static List extractSegments(String path) { + Matcher matcher = ARRAY_INDEX_PATTERN.matcher(path); + String processedPath; + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(buffer, ".[$1]"); + } + matcher.appendTail(buffer); + + if (buffer.length() > 0) { + processedPath = buffer.toString(); + } + else { + processedPath = path; + } + + return Arrays.asList(processedPath.indexOf('.') > -1 ? processedPath.split("\\.") + : new String[] { processedPath }); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java new file mode 100644 index 000000000..70a4f1d00 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -0,0 +1,277 @@ +/* + * 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.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A {@code FieldProcessor} processes a payload's fields, allowing them to be extracted + * and removed + * + * @author Andy Wilkinson + * + */ +class FieldProcessor { + + boolean hasField(FieldPath fieldPath, Map payload) { + final AtomicReference hasField = new AtomicReference(false); + traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { + + @Override + public boolean foundMatch(Match match) { + hasField.set(true); + return false; + } + + @Override + public boolean matchNotFound() { + return false; + } + + }); + return hasField.get(); + } + + Object extract(final FieldPath path, Map payload) { + final List matches = new ArrayList(); + traverse(new ProcessingContext(payload, path), new MatchCallback() { + + @Override + public boolean foundMatch(Match match) { + matches.add(match.getValue()); + return true; + } + + @Override + public boolean matchNotFound() { + return false; + } + + }); + if (matches.isEmpty()) { + throw new IllegalArgumentException( + "The payload does not contain a field with the path '" + path + "'"); + } + if (path.isPrecise()) { + return matches.get(0); + } + else { + return matches; + } + } + + void remove(final FieldPath path, final Map payload) { + traverse(new ProcessingContext(payload, path), new MatchCallback() { + + @Override + public boolean foundMatch(Match match) { + match.remove(); + return true; + } + + @Override + public boolean matchNotFound() { + return true; + } + + }); + } + + private boolean traverse(ProcessingContext context, MatchCallback matchCallback) { + final String segment = context.getSegment(); + if (FieldPath.isArraySegment(segment)) { + if (context.getPayload() instanceof List) { + return handleListPayload(context, matchCallback); + } + } + else if (context.getPayload() instanceof Map + && ((Map) context.getPayload()).containsKey(segment)) { + return handleMapPayload(context, matchCallback); + } + + return matchCallback.matchNotFound(); + } + + private boolean handleListPayload(ProcessingContext context, + MatchCallback matchCallback) { + List list = context.getPayload(); + final Iterator items = list.iterator(); + if (context.isLeaf()) { + while (items.hasNext()) { + Object item = items.next(); + if (!matchCallback.foundMatch(new ListMatch(items, list, item, context + .getParentMatch()))) { + return false; + } + ; + } + return true; + } + else { + boolean result = true; + while (items.hasNext() && result) { + Object item = items.next(); + result = result + && traverse(context.descend(item, new ListMatch(items, list, + item, context.parent)), matchCallback); + } + return result; + } + } + + private boolean handleMapPayload(ProcessingContext context, + MatchCallback matchCallback) { + Map map = context.getPayload(); + final Object item = map.get(context.getSegment()); + MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), + context.getParentMatch()); + if (context.isLeaf()) { + return matchCallback.foundMatch(mapMatch); + } + else { + return traverse(context.descend(item, mapMatch), matchCallback); + } + } + + private final class MapMatch implements Match { + + private final Object item; + + private final Map map; + + private final String segment; + + private final Match parent; + + private MapMatch(Object item, Map map, String segment, Match parent) { + this.item = item; + this.map = map; + this.segment = segment; + this.parent = parent; + } + + @Override + public Object getValue() { + return this.item; + } + + @Override + public void remove() { + this.map.remove(this.segment); + if (this.map.isEmpty() && this.parent != null) { + this.parent.remove(); + } + } + + } + + private final class ListMatch implements Match { + + private final Iterator items; + + private final List list; + + private final Object item; + + private final Match parent; + + private ListMatch(Iterator items, List list, Object item, Match parent) { + this.items = items; + this.list = list; + this.item = item; + this.parent = parent; + } + + @Override + public Object getValue() { + return this.item; + } + + @Override + public void remove() { + this.items.remove(); + if (this.list.isEmpty() && this.parent != null) { + this.parent.remove(); + } + } + + } + + private interface MatchCallback { + + boolean foundMatch(Match match); + + boolean matchNotFound(); + } + + private interface Match { + + Object getValue(); + + void remove(); + } + + private static class ProcessingContext { + + private final Object payload; + + private final List segments; + + private final Match parent; + + private final FieldPath path; + + private ProcessingContext(Object payload, FieldPath path) { + this(payload, path, null, null); + } + + private ProcessingContext(Object payload, FieldPath path, List segments, + Match parent) { + this.payload = payload; + this.path = path; + this.segments = segments == null ? path.getSegments() : segments; + this.parent = parent; + } + + private String getSegment() { + return this.segments.get(0); + } + + @SuppressWarnings("unchecked") + private T getPayload() { + return (T) this.payload; + } + + private boolean isLeaf() { + return this.segments.size() == 1; + } + + private Match getParentMatch() { + return this.parent; + } + + private ProcessingContext descend(Object payload, Match match) { + return new ProcessingContext(payload, this.path, this.segments.subList(1, + this.segments.size()), match); + } + } + +} 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 index 8444bc3de..7d9bd5337 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.Reader; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -46,8 +45,6 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver(); - private final FieldExtractor fieldExtractor = new FieldExtractor(); - private final FieldValidator fieldValidator = new FieldValidator(); private final ObjectMapper objectMapper = new ObjectMapper(); @@ -73,18 +70,6 @@ protected void handle(MvcResult result, DocumentationWriter writer) 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 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 index e93f08c1e..7eca8684d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java @@ -27,7 +27,7 @@ */ public enum FieldType { - ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING; + ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES; @Override public String toString() { 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 index 052aa8e72..89d2541b3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -26,10 +26,25 @@ */ class FieldTypeResolver { - private final FieldExtractor fieldExtractor = new FieldExtractor(); + private final FieldProcessor fieldProcessor = new FieldProcessor(); FieldType resolveFieldType(String path, Map payload) { - return determineFieldType(this.fieldExtractor.extractField(path, payload)); + FieldPath fieldPath = FieldPath.compile(path); + Object field = this.fieldProcessor.extract(fieldPath, payload); + if (field instanceof Collection && !fieldPath.isPrecise()) { + FieldType commonType = null; + for (Object item : (Collection) field) { + FieldType fieldType = determineFieldType(item); + if (commonType == null) { + commonType = fieldType; + } + else if (fieldType != commonType) { + return FieldType.VARIES; + } + } + return commonType; + } + return determineFieldType(this.fieldProcessor.extract(fieldPath, payload)); } private FieldType determineFieldType(Object fieldValue) { 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 index 469d0f6cd..075830490 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -34,7 +33,7 @@ */ class FieldValidator { - private final FieldExtractor fieldExtractor = new FieldExtractor(); + private final FieldProcessor fieldProcessor = new FieldProcessor(); private final ObjectMapper objectMapper = new ObjectMapper() .enable(SerializationFeature.INDENT_OUTPUT); @@ -69,7 +68,8 @@ private List findMissingFields(Map payload, for (FieldDescriptor fieldDescriptor : fieldDescriptors) { if (!fieldDescriptor.isOptional() - && !this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) { + && !this.fieldProcessor.hasField( + FieldPath.compile(fieldDescriptor.getPath()), payload)) { missingFields.add(fieldDescriptor.getPath()); } } @@ -80,33 +80,12 @@ private List findMissingFields(Map payload, 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); + FieldPath path = FieldPath.compile(fieldDescriptor.getPath()); + this.fieldProcessor.remove(path, 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 { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 774d23a05..217825cad 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -35,6 +35,56 @@ private PayloadDocumentation() { /** * Creates a {@code FieldDescriptor} that describes a field with the given * {@code path}. + *

+ * The {@code path} uses '.' to descend into a child object and ' {@code []}' to + * descend into an array. For example, with this JSON payload: + * + *

+	 * {
+     *    "a":{
+     *        "b":[
+     *            {
+     *                "c":"one"
+     *            },
+     *            {
+     *                "c":"two"
+     *            },
+     *            {
+     *                "d":"three"
+     *            }
+     *        ]
+     *    }
+     * }
+	 * 
+ * + * The following paths are all present: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PathValue
{@code a}An object containing "b"
{@code a.b}An array containing three objects
{@code a.b[]}An array containing three objects
{@code a.b[].c}An array containing the strings "one" and "two"
{@code a.b[].d}The string "three"
* * @param path The path of the field * @return a {@code FieldDescriptor} ready for further configuration diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java new file mode 100644 index 000000000..e082aaf1e --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java @@ -0,0 +1,61 @@ +/* + * 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.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Tests for {@link FieldPath} + * + * @author Andy Wilkinson + */ +public class FieldPathTests { + + @Test + public void singleFieldIsPrecise() { + assertTrue(FieldPath.compile("a").isPrecise()); + } + + @Test + public void singleNestedFieldIsPrecise() { + assertTrue(FieldPath.compile("a.b").isPrecise()); + } + + @Test + public void arrayIsNotPrecise() { + assertFalse(FieldPath.compile("a[]").isPrecise()); + } + + @Test + public void nestedArrayIsNotPrecise() { + assertFalse(FieldPath.compile("a.b[]").isPrecise()); + } + + @Test + public void arrayOfArraysIsNotPrecise() { + assertFalse(FieldPath.compile("a[][]").isPrecise()); + } + + @Test + public void fieldBeneathAnArrayIsNotPrecise() { + assertFalse(FieldPath.compile("a[].b").isPrecise()); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java new file mode 100644 index 000000000..949ae10fa --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java @@ -0,0 +1,233 @@ +/* + * 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.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Tests for {@link FieldProcessor} + * + * @author Andy Wilkinson + */ +public class FieldProcessorTests { + + private final FieldProcessor fieldProcessor = new FieldProcessor(); + + @Test + public void extractTopLevelMapEntry() { + Map payload = new HashMap<>(); + payload.put("a", "alpha"); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload), + equalTo((Object) "alpha")); + } + + @Test + public void extractNestedMapEntry() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a.b"), payload), + equalTo((Object) "bravo")); + } + + @Test + public void extractArray() { + Map payload = new HashMap<>(); + Map bravo = new HashMap<>(); + bravo.put("b", "bravo"); + List> alpha = Arrays.asList(bravo, bravo); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload), + equalTo((Object) alpha)); + } + + @Test + public void extractArrayContents() { + Map payload = new HashMap<>(); + Map bravo = new HashMap<>(); + bravo.put("b", "bravo"); + List> alpha = Arrays.asList(bravo, bravo); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a[]"), payload), + equalTo((Object) alpha)); + } + + @Test + public void extractFromItemsInArray() { + Map payload = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("b", "bravo"); + List> alpha = Arrays.asList(entry, entry); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a[].b"), payload), + equalTo((Object) Arrays.asList("bravo", "bravo"))); + } + + @Test + public void extractNestedArray() { + Map payload = new HashMap<>(); + Map entry1 = createEntry("id:1"); + Map entry2 = createEntry("id:2"); + Map entry3 = createEntry("id:3"); + List>> alpha = Arrays.asList( + Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][]"), payload), + equalTo((Object) Arrays.asList(entry1, entry2, entry3))); + } + + @Test + public void extractFromItemsInNestedArray() { + Map payload = new HashMap<>(); + Map entry1 = createEntry("id:1"); + Map entry2 = createEntry("id:2"); + Map entry3 = createEntry("id:3"); + List>> alpha = Arrays.asList( + Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].id"), payload), + equalTo((Object) Arrays.asList("1", "2", "3"))); + } + + @Test + public void extractArraysFromItemsInNestedArray() { + Map payload = new HashMap<>(); + Map entry1 = createEntry("ids", Arrays.asList(1, 2)); + Map entry2 = createEntry("ids", Arrays.asList(3)); + Map entry3 = createEntry("ids", Arrays.asList(4)); + List>> alpha = Arrays.asList( + Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].ids"), payload), + equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), + Arrays.asList(4)))); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentTopLevelField() { + this.fieldProcessor + .extract(FieldPath.compile("a"), new HashMap()); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentNestedField() { + HashMap payload = new HashMap(); + payload.put("a", new HashMap()); + this.fieldProcessor.extract(FieldPath.compile("a.b"), payload); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentNestedFieldWhenParentIsNotAMap() { + HashMap payload = new HashMap(); + payload.put("a", 5); + this.fieldProcessor.extract(FieldPath.compile("a.b"), payload); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentFieldWhenParentIsAnArray() { + HashMap payload = new HashMap(); + HashMap alpha = new HashMap(); + alpha.put("b", Arrays.asList(new HashMap())); + payload.put("a", alpha); + this.fieldProcessor.extract(FieldPath.compile("a.b.c"), payload); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentArrayField() { + HashMap payload = new HashMap(); + this.fieldProcessor.extract(FieldPath.compile("a[]"), payload); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentArrayFieldAsTypeDoesNotMatch() { + HashMap payload = new HashMap(); + payload.put("a", 5); + this.fieldProcessor.extract(FieldPath.compile("a[]"), payload); + } + + @Test(expected = IllegalArgumentException.class) + public void nonExistentFieldBeneathAnArray() { + HashMap payload = new HashMap(); + HashMap alpha = new HashMap(); + alpha.put("b", Arrays.asList(new HashMap())); + payload.put("a", alpha); + this.fieldProcessor.extract(FieldPath.compile("a.b[].id"), payload); + } + + @Test + public void removeTopLevelMapEntry() { + Map payload = new HashMap<>(); + payload.put("a", "alpha"); + this.fieldProcessor.remove(FieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @Test + public void removeNestedMapEntry() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.remove(FieldPath.compile("a.b"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeItemsInArray() throws IOException { + Map payload = new ObjectMapper().readValue( + "{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + this.fieldProcessor.remove(FieldPath.compile("a[].b"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeItemsInNestedArray() throws IOException { + Map payload = new ObjectMapper().readValue( + "{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); + this.fieldProcessor.remove(FieldPath.compile("a[][].id"), payload); + assertThat(payload.size(), equalTo(0)); + } + + private Map createEntry(String... pairs) { + Map entry = new HashMap<>(); + for (String pair : pairs) { + String[] components = pair.split(":"); + entry.put(components[0], components[1]); + } + return entry; + } + + private Map createEntry(String key, Object value) { + Map entry = new HashMap<>(); + entry.put(key, value); + return entry; + } +} 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 index 667225f8e..42d5fc9f8 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -77,6 +77,20 @@ public void nestedField() throws IOException { createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT)); } + @Test + public void multipleFieldsWithSameType() throws IOException { + assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), + equalTo(FieldType.NUMBER)); + } + + @Test + public void multipleFieldsWithDifferentTypes() throws IOException { + assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), + equalTo(FieldType.VARIES)); + } + @Test public void nonExistentFieldProducesIllegalArgumentException() throws IOException { this.thrownException.expect(IllegalArgumentException.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 index 9af51c0e1..e6f2f43fc 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -38,26 +38,28 @@ public class FieldValidatorTests { @Rule public ExpectedException thrownException = ExpectedException.none(); - private StringReader payload = new StringReader("{\"a\":{\"b\":{}, \"c\":true}}"); + private StringReader payload = new StringReader( + "{\"a\":{\"b\":{},\"c\":true,\"d\":[{\"e\":1},{\"e\":2}]}}"); @Test public void noMissingFieldsAllFieldsDocumented() throws IOException { - this.fieldValidator.validate(this.payload, Arrays.asList( - new FieldDescriptor("a"), new FieldDescriptor("a.b"), - new FieldDescriptor("a.c"))); + this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor( + "a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"), + new FieldDescriptor("a.d"), new FieldDescriptor("a"))); } @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())); + new FieldDescriptor("a.c"), new FieldDescriptor("a.d"), + 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"))); + this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor( + "a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"))); } @Test @@ -83,6 +85,6 @@ public void undocumentedField() throws IOException { .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"))); + Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.d"))); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java new file mode 100644 index 000000000..6e45e20f6 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -0,0 +1,155 @@ +/* + * 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.Matchers.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.hamcrest.Matcher; +import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.StubMvcResult; +import org.springframework.util.StringUtils; + +/** + * Tests for {@link PayloadDocumentation} + * + * @author Andy Wilkinson + */ +public class PayloadDocumentationTests { + + private final File outputDir = new File("build/payload-documentation-tests"); + + @Before + public void setup() { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + } + + @After + public void cleanup() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void requestWithFields() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); + documentRequestFields("request-with-fields", + fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three")).handle( + new StubMvcResult(request, null)); + assertThat( + snippet("request-with-fields", "request-fields"), + is(asciidoctorTableWith(header("Path", "Type", "Description"), + row("a.b", "Number", "one"), row("a.c", "String", "two"), + row("a", "Object", "three")))); + } + + @Test + public void responseWithFields() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter() + .append("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\": [{\"id\":356,\"name\": \"sample\"}]}"); + documentResponseFields("response-with-fields", + fieldWithPath("id").description("one"), + fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three"), + fieldWithPath("assets[]").description("four"), + fieldWithPath("assets[].id").description("five"), + fieldWithPath("assets[].name").description("six")).handle( + new StubMvcResult(new MockHttpServletRequest("GET", "/"), response)); + assertThat( + snippet("response-with-fields", "response-fields"), + is(asciidoctorTableWith(header("Path", "Type", "Description"), + row("id", "Number", "one"), row("date", "String", "two"), + row("assets", "Array", "three"), + row("assets[]", "Object", "four"), + row("assets[].id", "Number", "five"), + row("assets[].name", "String", "six")))); + } + + private Matcher> asciidoctorTableWith(String[] header, + String[]... rows) { + Collection> matchers = new ArrayList>(); + for (String headerItem : header) { + matchers.add(equalTo(headerItem)); + } + + for (String[] row : rows) { + for (String rowItem : row) { + matchers.add(equalTo(rowItem)); + } + } + + matchers.add(equalTo("|===")); + matchers.add(equalTo("")); + + return new IsIterableContainingInAnyOrder(matchers); + } + + private String[] header(String... columns) { + String header = "|" + + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + return new String[] { "", "|===", header, "" }; + } + + private String[] row(String... entries) { + List lines = new ArrayList(); + for (String entry : entries) { + lines.add("|" + entry); + } + lines.add(""); + return lines.toArray(new String[lines.size()]); + } + + private List snippet(String snippetName, String snippetType) + throws IOException { + File snippetDir = new File(this.outputDir, snippetName); + File snippetFile = new File(snippetDir, snippetType + ".adoc"); + String line = null; + List lines = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); + try { + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + finally { + reader.close(); + } + return lines; + } + +} From da7efa3e3a7362101a50912ed0b9c25e21532444 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Apr 2015 16:26:01 +0100 Subject: [PATCH 0059/1059] Improve testing of the generated documentation snippets --- .../restdocs/RestDocumentationException.java | 37 +++ .../restdocs/http/HttpDocumentation.java | 5 +- .../hypermedia/LinkSnippetResultHandler.java | 8 +- .../restdocs/payload/FieldProcessor.java | 1 - .../restdocs/payload/FieldValidator.java | 17 +- .../restdocs/curl/CurlDocumentationTests.java | 214 +++++++----------- .../restdocs/http/HttpDocumentationTests.java | 146 +++++------- .../HypermediaDocumentationTests.java | 117 ++++++++++ .../restdocs/payload/FieldValidatorTests.java | 15 +- .../payload/PayloadDocumentationTests.java | 157 ++++++------- .../restdocs/test/ExpectedSnippet.java | 193 ++++++++++++++++ .../restdocs/test/SnippetMatchers.java | 177 +++++++++++++++ .../restdocs/{ => test}/StubMvcResult.java | 38 +++- 13 files changed, 780 insertions(+), 345 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{ => test}/StubMvcResult.java (63%) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java new file mode 100644 index 000000000..f63c1f22f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * A {@link RuntimeException} thrown to indicate a problem with a RESTful resource's + * documentation. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("serial") +public class RestDocumentationException extends RuntimeException { + + /** + * Creates a new {@code RestDocumentationException} described by the given + * {@code message} + * @param message the message that describes the documentation problem + */ + public RestDocumentationException(String message) { + super(message); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index d840f925a..6726da47f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -148,7 +148,10 @@ public void perform() throws IOException { } } this.writer.println(); - this.writer.println(this.result.getResponse().getContentAsString()); + String content = this.result.getResponse().getContentAsString(); + if (StringUtils.hasText(content)) { + this.writer.println(content); + } } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index d2c1f2c01..0940ccfb8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.hypermedia; -import static org.junit.Assert.fail; - import java.io.IOException; import java.util.HashMap; import java.util.HashSet; @@ -26,6 +24,7 @@ import java.util.Map.Entry; import java.util.Set; +import org.springframework.restdocs.RestDocumentationException; import org.springframework.restdocs.snippet.DocumentationWriter; import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; @@ -94,10 +93,13 @@ protected void handle(MvcResult result, DocumentationWriter writer) + undocumentedRels; } if (!missingRels.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } message += "Links with the following relations were not found in the response: " + missingRels; } - fail(message); + throw new RestDocumentationException(message); } Assert.isTrue(actualRels.equals(expectedRels)); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java index 70a4f1d00..eb9e0ca86 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -121,7 +121,6 @@ private boolean handleListPayload(ProcessingContext context, .getParentMatch()))) { return false; } - ; } return true; } 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 index 075830490..e8de809fd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.RestDocumentationException; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -51,14 +53,17 @@ void validate(Reader payloadReader, List fieldDescriptors) String message = ""; if (!undocumentedPayload.isEmpty()) { message += String.format( - "Portions of the payload were not documented:%n%s", + "The following parts of the payload were not documented:%n%s", this.objectMapper.writeValueAsString(undocumentedPayload)); } if (!missingFields.isEmpty()) { + if (message.length() > 0) { + message += String.format("%n"); + } message += "Fields with the following paths were not found in the payload: " + missingFields; } - throw new FieldValidationException(message); + throw new RestDocumentationException(message); } } @@ -86,12 +91,4 @@ private Map findUndocumentedFields(Map payload, return payload; } - @SuppressWarnings("serial") - static class FieldValidationException extends RuntimeException { - - FieldValidationException(String message) { - super(message); - } - } - } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 296408c5e..e984e8302 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -16,23 +16,19 @@ package org.springframework.restdocs.curl; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.junit.Assert.assertThat; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.StubMvcResult; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link CurlDocumentation} @@ -43,224 +39,174 @@ */ public class CurlDocumentationTests { - private final File outputDir = new File("build/curl-documentation-tests"); - - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void getRequest() throws IOException { - documentCurlRequest("get-request").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); - assertThat(requestSnippetLines("get-request"), - hasItem("$ curl 'http://localhost/foo' -i")); + this.snippet.expectCurlRequest("get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); + documentCurlRequest("get-request").handle(result(get("/foo"))); } @Test public void nonGetRequest() throws IOException { - documentCurlRequest("non-get-request").handle( - new StubMvcResult(new MockHttpServletRequest("POST", "/foo"), null)); - assertThat(requestSnippetLines("non-get-request"), - hasItem("$ curl 'http://localhost/foo' -i -X POST")); + this.snippet.expectCurlRequest("non-get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); + documentCurlRequest("non-get-request").handle(result(post("/foo"))); } @Test public void requestWithContent() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContent("content".getBytes()); + this.snippet.expectCurlRequest("request-with-content").withContents( + codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -d 'content'")); documentCurlRequest("request-with-content").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-content"), - hasItem("$ curl 'http://localhost/foo' -i -d 'content'")); - } - - @Test - public void requestWitUriQueryString() throws IOException { - documentCurlRequest("request-with-uri-query-string").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo?param=value"), - null)); - assertThat(requestSnippetLines("request-with-uri-query-string"), - hasItem("$ curl 'http://localhost/foo?param=value' -i")); + result(get("/foo").content("content"))); } @Test public void requestWithQueryString() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setQueryString("param=value"); + this.snippet.expectCurlRequest("request-with-query-string") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i")); documentCurlRequest("request-with-query-string").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-query-string"), - hasItem("$ curl 'http://localhost/foo?param=value' -i")); + result(get("/foo?param=value"))); } @Test public void requestWithOneParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "v1"); + this.snippet.expectCurlRequest("request-with-one-parameter").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); documentCurlRequest("request-with-one-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-one-parameter"), - hasItem("$ curl 'http://localhost/foo?k1=v1' -i")); + result(get("/foo").param("k1", "v1"))); } @Test public void requestWithMultipleParameters() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "v1"); - request.addParameter("k2", "v2"); - request.addParameter("k1", "v1-bis"); + this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); documentCurlRequest("request-with-multiple-parameters").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-multiple-parameters"), - hasItem("$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); + result(get("/foo").param("k1", "v1").param("k2", "v2") + .param("k1", "v1-bis"))); } @Test public void requestWithUrlEncodedParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("k1", "foo bar&"); + this.snippet.expectCurlRequest("request-with-url-encoded-parameter") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); documentCurlRequest("request-with-url-encoded-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-url-encoded-parameter"), - hasItem("$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); + result(get("/foo").param("k1", "foo bar&"))); } @Test public void postRequestWithOneParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "v1"); + this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); documentCurlRequest("post-request-with-one-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-one-parameter"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + result(post("/foo").param("k1", "v1"))); } @Test public void postRequestWithMultipleParameters() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "v1"); - request.addParameter("k2", "v2"); - request.addParameter("k1", "v1-bis"); + this.snippet.expectCurlRequest("post-request-with-multiple-parameters") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); documentCurlRequest("post-request-with-multiple-parameters").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("post-request-with-multiple-parameters"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1&k1=v1-bis&k2=v2'")); + result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("k1", "a&b"); + this.snippet + .expectCurlRequest("post-request-with-url-encoded-parameter") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); documentCurlRequest("post-request-with-url-encoded-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-url-encoded-parameter"), - hasItem("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + result(post("/foo").param("k1", "a&b"))); } @Test public void requestWithHeaders() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContentType(MediaType.APPLICATION_JSON_VALUE); - request.addHeader("a", "alpha"); + this.snippet.expectCurlRequest("request-with-headers").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); documentCurlRequest("request-with-headers").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("request-with-headers"), - hasItem("$ curl 'http://localhost/foo' -i -H 'Content-Type: application/json' -H 'a: alpha'")); + result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", + "alpha"))); } @Test public void httpWithNonStandardPort() throws IOException { + this.snippet.expectCurlRequest("http-with-non-standard-port").withContents( + codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8080); - documentCurlRequest("http-with-non-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("http-with-non-standard-port"), - hasItem("$ curl 'http://localhost:8080/foo' -i")); + documentCurlRequest("http-with-non-standard-port").handle(result(request)); } @Test public void httpsWithStandardPort() throws IOException { + this.snippet.expectCurlRequest("https-with-standard-port").withContents( + codeBlock("bash").content("$ curl 'https://localhost/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(443); request.setScheme("https"); - documentCurlRequest("https-with-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("https-with-standard-port"), - hasItem("$ curl 'https://localhost/foo' -i")); + documentCurlRequest("https-with-standard-port").handle(result(request)); } @Test public void httpsWithNonStandardPort() throws IOException { + this.snippet.expectCurlRequest("https-with-non-standard-port").withContents( + codeBlock("bash").content("$ curl 'https://localhost:8443/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8443); request.setScheme("https"); - documentCurlRequest("https-with-non-standard-port").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("https-with-non-standard-port"), - hasItem("$ curl 'https://localhost:8443/foo' -i")); + documentCurlRequest("https-with-non-standard-port").handle(result(request)); } @Test public void requestWithCustomHost() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-host").withContents( + codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentCurlRequest("request-with-custom-host").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-host"), - hasItem("$ curl 'http://api.example.com/foo' -i")); + documentCurlRequest("request-with-custom-host").handle(result(request)); } @Test public void requestWithContextPathWithSlash() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-context-with-slash") + .withContents( + codeBlock("bash").content( + "$ curl 'http://api.example.com/v3/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("/v3"); documentCurlRequest("request-with-custom-context-with-slash").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-context-with-slash"), - hasItem("$ curl 'http://api.example.com/v3/foo' -i")); + result(request)); } @Test public void requestWithContextPathWithoutSlash() throws IOException { + this.snippet.expectCurlRequest("request-with-custom-context-without-slash") + .withContents( + codeBlock("bash").content( + "$ curl 'http://api.example.com/v3/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("v3"); documentCurlRequest("request-with-custom-context-without-slash").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("request-with-custom-context-without-slash"), - hasItem("$ curl 'http://api.example.com/v3/foo' -i")); - } - - private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "curl-request"); + result(request)); } - private List snippetLines(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index d0fc8ec20..4211c909c 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -16,27 +16,25 @@ package org.springframework.restdocs.http; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.junit.Assert.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.StubMvcResult; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link HttpDocumentation} @@ -45,131 +43,93 @@ */ public class HttpDocumentationTests { - private final File outputDir = new File("build/http-documentation-tests"); - - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void getRequest() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addHeader("Alpha", "a"); - documentHttpRequest("get-request").handle(new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request"), - hasItems("GET /foo HTTP/1.1", "Alpha: a")); + this.snippet.expectHttpRequest("get-request").withContents( + httpRequest(GET, "/foo").header("Alpha", "a")); + + documentHttpRequest("get-request").handle( + result(get("/foo").header("Alpha", "a"))); } @Test public void getRequestWithQueryString() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo?bar=baz"); + this.snippet.expectHttpRequest("get-request-with-query-string").withContents( + httpRequest(GET, "/foo?bar=baz")); + documentHttpRequest("get-request-with-query-string").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request-with-query-string"), - hasItems("GET /foo?bar=baz HTTP/1.1")); + result(get("/foo?bar=baz"))); } @Test public void getRequestWithParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.addParameter("b&r", "baz"); + this.snippet.expectHttpRequest("get-request-with-parameter").withContents( + httpRequest(GET, "/foo?b%26r=baz")); + documentHttpRequest("get-request-with-parameter").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("get-request-with-parameter"), - hasItems("GET /foo?b%26r=baz HTTP/1.1")); + result(get("/foo").param("b&r", "baz"))); } @Test public void postRequestWithContent() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - byte[] content = "Hello, world".getBytes(); - request.setContent(content); + this.snippet.expectHttpRequest("post-request-with-content").withContents( + httpRequest(POST, "/foo") // + .content("Hello, world")); + documentHttpRequest("post-request-with-content").handle( - new StubMvcResult(request, null)); - assertThat(requestSnippetLines("post-request-with-content"), - hasItems("POST /foo HTTP/1.1", "Hello, world")); + result(post("/foo").content("Hello, world"))); } @Test public void postRequestWithParameter() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); - request.addParameter("b&r", "baz"); - request.addParameter("a", "alpha"); + this.snippet.expectHttpRequest("post-request-with-parameter").withContents( + httpRequest(POST, "/foo") // + .header("Content-Type", "application/x-www-form-urlencoded") // + .content("b%26r=baz&a=alpha")); + documentHttpRequest("post-request-with-parameter").handle( - new StubMvcResult(request, null)); - assertThat( - requestSnippetLines("post-request-with-parameter"), - hasItems("POST /foo HTTP/1.1", - "Content-Type: application/x-www-form-urlencoded", - "b%26r=baz&a=alpha")); + result(post("/foo").param("b&r", "baz").param("a", "alpha"))); } @Test public void basicResponse() throws IOException { - documentHttpResponse("basic-response").handle( - new StubMvcResult(null, new MockHttpServletResponse())); - assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); + this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); + documentHttpResponse("basic-response").handle(result()); } @Test public void nonOkResponse() throws IOException { + this.snippet.expectHttpResponse("non-ok-response").withContents( + httpResponse(BAD_REQUEST)); + MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItem("HTTP/1.1 400 Bad Request")); + response.setStatus(BAD_REQUEST.value()); + documentHttpResponse("non-ok-response").handle(result(response)); } @Test public void responseWithHeaders() throws IOException { + this.snippet.expectHttpResponse("response-with-headers").withContents( + httpResponse(OK) // + .header("Content-Type", "application/json") // + .header("a", "alpha")); + MockHttpServletResponse response = new MockHttpServletResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setHeader("a", "alpha"); - documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); + documentHttpResponse("response-with-headers").handle(result(response)); } @Test public void responseWithContent() throws IOException { + this.snippet.expectHttpResponse("response-with-content").withContents( + httpResponse(OK).content("content")); MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().append("content"); - documentHttpResponse("response-with-content").handle( - new StubMvcResult(null, response)); - assertThat(responseSnippetLines("response-with-content"), - hasItems("HTTP/1.1 200 OK", "content")); - } - - private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "http-request"); - } - - private List responseSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "http-response"); + documentHttpResponse("response-with-content").handle(result(response)); } - private List snippetLines(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java new file mode 100644 index 000000000..15073949f --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -0,0 +1,117 @@ +/* + * 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.hypermedia; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link HypermediaDocumentation} + * + * @author Andy Wilkinson + */ +public class HypermediaDocumentationTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [foo]")); + documentLinks("undocumented-link", + new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle( + result()); + } + + @Test + public void missingLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " found in the response: [foo]")); + documentLinks("undocumented-link", new StubLinkExtractor(), + new LinkDescriptor("foo").description("bar")).handle(result()); + } + + @Test + public void undocumentedLinkAndMissingLink() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [a]. Links with the following relations were not" + + " found in the response: [foo]")); + documentLinks("undocumented-link-and-missing-link", + new StubLinkExtractor().withLinks(new Link("a", "alpha")), + new LinkDescriptor("foo").description("bar")).handle(result()); + } + + @Test + public void documentedLinks() throws IOException { + this.snippet.expectLinks("documented-links").withContents( // + tableWithHeader("Relation", "Description") // + .row("a", "one") // + .row("b", "two")); + documentLinks( + "documented-links", + new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", + "bravo")), new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")).handle(result()); + } + + private static class StubLinkExtractor implements LinkExtractor { + + private Map> linksByRel = new HashMap>(); + + @Override + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + return this.linksByRel; + } + + private StubLinkExtractor withLinks(Link... links) { + for (Link link : links) { + List linksWithRel = this.linksByRel.get(link.getRel()); + if (linksWithRel == null) { + linksWithRel = new ArrayList(); + this.linksByRel.put(link.getRel(), linksWithRel); + } + linksWithRel.add(link); + } + return this; + } + + } + +} 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 index e6f2f43fc..2706a8324 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -24,7 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.payload.FieldValidator.FieldValidationException; +import org.springframework.restdocs.RestDocumentationException; /** * Tests for {@link FieldValidator} @@ -70,9 +70,10 @@ public void childIsDocumentedWhenParentIsDocumented() throws IOException { @Test public void missingField() throws IOException { - this.thrownException.expect(FieldValidationException.class); + this.thrownException.expect(RestDocumentationException.class); this.thrownException - .expectMessage(equalTo("Fields with the following paths were not found in the payload: [y, z]")); + .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"))); @@ -80,10 +81,10 @@ public void missingField() throws IOException { @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.thrownException.expect(RestDocumentationException.class); + this.thrownException.expectMessage(equalTo(String + .format("The following parts 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"), new FieldDescriptor("a.d"))); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 6e45e20f6..fd3be61fa 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -15,31 +15,24 @@ */ package org.springframework.restdocs.payload; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import org.hamcrest.Matcher; -import org.hamcrest.collection.IsIterableContainingInAnyOrder; -import org.junit.After; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; +import org.junit.rules.ExpectedException; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.StubMvcResult; -import org.springframework.util.StringUtils; +import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.test.ExpectedSnippet; /** * Tests for {@link PayloadDocumentation} @@ -48,40 +41,42 @@ */ public class PayloadDocumentationTests { - private final File outputDir = new File("build/payload-documentation-tests"); + @Rule + public final ExpectedException thrown = ExpectedException.none(); - @Before - public void setup() { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - } - - @After - public void cleanup() { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); @Test public void requestWithFields() throws IOException { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); + this.snippet.expectRequestFields("request-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("a.b", "Number", "one") // + .row("a.c", "String", "two") // + .row("a", "Object", "three")); + documentRequestFields("request-with-fields", fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three")).handle( - new StubMvcResult(request, null)); - assertThat( - snippet("request-with-fields", "request-fields"), - is(asciidoctorTableWith(header("Path", "Type", "Description"), - row("a.b", "Number", "one"), row("a.c", "String", "two"), - row("a", "Object", "three")))); + result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"))); } @Test public void responseWithFields() throws IOException { + this.snippet.expectResponseFields("response-with-fields").withContents(// + tableWithHeader("Path", "Type", "Description") // + .row("id", "Number", "one") // + .row("date", "String", "two") // + .row("assets", "Array", "three") // + .row("assets[]", "Object", "four") // + .row("assets[].id", "Number", "five") // + .row("assets[].name", "String", "six")); + MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter() - .append("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\": [{\"id\":356,\"name\": \"sample\"}]}"); + response.getWriter().append( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}"); documentResponseFields("response-with-fields", fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), @@ -89,67 +84,41 @@ public void responseWithFields() throws IOException { fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six")).handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/"), response)); - assertThat( - snippet("response-with-fields", "response-fields"), - is(asciidoctorTableWith(header("Path", "Type", "Description"), - row("id", "Number", "one"), row("date", "String", "two"), - row("assets", "Array", "three"), - row("assets[]", "Object", "four"), - row("assets[].id", "Number", "five"), - row("assets[].name", "String", "six")))); - } - - private Matcher> asciidoctorTableWith(String[] header, - String[]... rows) { - Collection> matchers = new ArrayList>(); - for (String headerItem : header) { - matchers.add(equalTo(headerItem)); - } - - for (String[] row : rows) { - for (String rowItem : row) { - matchers.add(equalTo(rowItem)); - } - } - - matchers.add(equalTo("|===")); - matchers.add(equalTo("")); - - return new IsIterableContainingInAnyOrder(matchers); + result(response)); } - private String[] header(String... columns) { - String header = "|" - + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); - return new String[] { "", "|===", header, "" }; + @Test + public void undocumentedRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + documentRequestFields("undocumented-request-fields").handle( + result(get("/foo").content("{\"a\": 5}"))); } - private String[] row(String... entries) { - List lines = new ArrayList(); - for (String entry : entries) { - lines.add("|" + entry); - } - lines.add(""); - return lines.toArray(new String[lines.size()]); + @Test + public void missingRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a.b]")); + documentRequestFields("missing-request-fields", + fieldWithPath("a.b").description("one")).handle( + result(get("/foo").content("{}"))); } - private List snippet(String snippetName, String snippetType) - throws IOException { - File snippetDir = new File(this.outputDir, snippetName); - File snippetFile = new File(snippetDir, snippetType + ".adoc"); - String line = null; - List lines = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); - try { - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } - finally { - reader.close(); - } - return lines; + @Test + public void undocumentedRequestFieldAndMissingRequestField() throws IOException { + this.thrown.expect(RestDocumentationException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a.b]")); + documentRequestFields("undocumented-request-field-and-missing-request-field", + fieldWithPath("a.b").description("one")).handle( + result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); } - } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java new file mode 100644 index 000000000..5c7f9b52f --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -0,0 +1,193 @@ +/* + * 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.test; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.hamcrest.Matcher; +import org.hamcrest.collection.IsIterableContainingInAnyOrder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * The {@code ExpectedSnippet} rule is used to verify that a + * {@link SnippetWritingResultHandler} has generated the expected snippet. + * + * @author Andy Wilkinson + */ +public class ExpectedSnippet implements TestRule { + + private String expectedName; + + private String expectedType; + + private Matcher expectedContents; + + private File outputDir; + + @Override + public Statement apply(final Statement base, Description description) { + this.outputDir = new File("build/" + description.getTestClass().getSimpleName()); + return new OutputDirectoryStatement(new ExpectedSnippetStatement(base), + this.outputDir); + } + + private static final class OutputDirectoryStatement extends Statement { + + private final Statement delegate; + + private final File outputDir; + + public OutputDirectoryStatement(Statement delegate, File outputDir) { + this.delegate = delegate; + this.outputDir = outputDir; + } + + @Override + public void evaluate() throws Throwable { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + try { + this.delegate.evaluate(); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + } + + private final class ExpectedSnippetStatement extends Statement { + + private final Statement delegate; + + public ExpectedSnippetStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + this.delegate.evaluate(); + verifySnippet(); + } + + } + + private void verifySnippet() throws IOException { + if (this.outputDir != null && this.expectedName != null) { + File snippetDir = new File(this.outputDir, this.expectedName); + File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); + assertTrue("The file " + snippetFile + " does not exist or is not a file", + snippetFile.isFile()); + if (this.expectedContents != null) { + assertThat(read(snippetFile), this.expectedContents); + } + } + } + + public ExpectedSnippet expectCurlRequest(String name) { + expect(name, "curl-request"); + return this; + } + + public ExpectedSnippet expectRequestFields(String name) { + expect(name, "request-fields"); + return this; + } + + public ExpectedSnippet expectResponseFields(String name) { + expect(name, "response-fields"); + return this; + } + + public ExpectedSnippet expectLinks(String name) { + expect(name, "links"); + return this; + } + + public ExpectedSnippet expectHttpRequest(String name) { + expect(name, "http-request"); + return this; + } + + public ExpectedSnippet expectHttpResponse(String name) { + expect(name, "http-response"); + return this; + } + + private ExpectedSnippet expect(String name, String type) { + this.expectedName = name; + this.expectedType = type; + return this; + } + + public void withContents(Matcher matcher) { + this.expectedContents = matcher; + } + + private String read(File snippetFile) throws IOException { + return FileCopyUtils.copyToString(new FileReader(snippetFile)); + } + + public Matcher> asciidoctorTableWith(String[] header, + String[]... rows) { + Collection> matchers = new ArrayList>(); + for (String headerItem : header) { + matchers.add(equalTo(headerItem)); + } + + for (String[] row : rows) { + for (String rowItem : row) { + matchers.add(equalTo(rowItem)); + } + } + + matchers.add(equalTo("|===")); + matchers.add(equalTo("")); + + return new IsIterableContainingInAnyOrder(matchers); + } + + public String[] header(String... columns) { + String header = "|" + + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + return new String[] { "", "|===", header, "" }; + } + + public String[] row(String... entries) { + List lines = new ArrayList(); + for (String entry : entries) { + lines.add("|" + entry); + } + lines.add(""); + return lines.toArray(new String[lines.size()]); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java new file mode 100644 index 000000000..801ad07f1 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -0,0 +1,177 @@ +/* + * 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.test; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * {@link Matcher Matchers} for verify the contents of generated documentation snippets. + * + * @author Andy Wilkinson + */ +public class SnippetMatchers { + + public static AsciidoctorTableMatcher tableWithHeader(String... headers) { + return new AsciidoctorTableMatcher(headers); + } + + public static HttpRequestMatcher httpRequest(RequestMethod method, String uri) { + return new HttpRequestMatcher(method, uri); + } + + public static HttpResponseMatcher httpResponse(HttpStatus status) { + return new HttpResponseMatcher(status); + } + + @SuppressWarnings({ "rawtypes" }) + public static AsciidoctorCodeBlockMatcher codeBlock(String language) { + return new AsciidoctorCodeBlockMatcher(language); + } + + private static abstract class AbstractSnippetMatcher extends BaseMatcher { + + private List lines = new ArrayList(); + + protected void addLine(String line) { + this.lines.add(line); + } + + protected void addLine(int index, String line) { + if (index < 0) { + index = index + this.lines.size(); + } + this.lines.add(index, line); + } + + @Override + public boolean matches(Object item) { + return getLinesAsString().equals(item); + } + + @Override + public void describeTo(Description description) { + description.appendText("Asciidoctor snippet"); + description.appendText(getLinesAsString()); + } + + @Override + public void describeMismatch(Object item, Description description) { + description.appendText("was:"); + if (item instanceof String) { + description.appendText((String) item); + } + else { + description.appendValue(item); + } + } + + private String getLinesAsString() { + StringWriter writer = new StringWriter(); + for (String line : this.lines) { + writer.append(String.format("%s%n", line)); + } + return writer.toString(); + } + } + + public static class AsciidoctorCodeBlockMatcher> + extends AbstractSnippetMatcher { + + protected AsciidoctorCodeBlockMatcher(String language) { + this.addLine(""); + this.addLine("[source," + language + "]"); + this.addLine("----"); + this.addLine("----"); + this.addLine(""); + } + + @SuppressWarnings("unchecked") + public T content(String content) { + this.addLine(-2, content); + return (T) this; + } + + } + + public static abstract class HttpMatcher> extends + AsciidoctorCodeBlockMatcher> { + + private int headerOffset = 4; + + protected HttpMatcher() { + super("http"); + } + + @SuppressWarnings("unchecked") + public T header(String name, String value) { + this.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + + } + + public static class HttpResponseMatcher extends HttpMatcher { + + public HttpResponseMatcher(HttpStatus status) { + this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); + this.content(""); + } + + } + + public static class HttpRequestMatcher extends HttpMatcher { + + public HttpRequestMatcher(RequestMethod requestMethod, String uri) { + this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); + this.content(""); + } + + } + + public static class AsciidoctorTableMatcher extends AbstractSnippetMatcher { + + private AsciidoctorTableMatcher(String... columns) { + this.addLine(""); + this.addLine("|==="); + String header = "|" + + StringUtils + .collectionToDelimitedString(Arrays.asList(columns), "|"); + this.addLine(header); + this.addLine(""); + this.addLine("|==="); + this.addLine(""); + } + + public AsciidoctorTableMatcher row(String... entries) { + for (String entry : entries) { + this.addLine(-2, "|" + entry); + } + this.addLine(-2, ""); + return this; + } + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java similarity index 63% rename from spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 1afea51e8..ab11f19d0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -14,11 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; @@ -35,11 +37,43 @@ public class StubMvcResult implements MvcResult { private final MockHttpServletResponse response; - public StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { + public static StubMvcResult result() { + return new StubMvcResult(); + } + + public static StubMvcResult result(RequestBuilder requestBuilder) { + return new StubMvcResult(requestBuilder); + } + + public static StubMvcResult result(MockHttpServletRequest request) { + return new StubMvcResult(request); + } + + public static StubMvcResult result(MockHttpServletResponse response) { + return new StubMvcResult(response); + } + + private StubMvcResult() { + this(new MockHttpServletRequest(), new MockHttpServletResponse()); + } + + private StubMvcResult(MockHttpServletRequest request) { + this(request, new MockHttpServletResponse()); + } + + private StubMvcResult(MockHttpServletResponse response) { + this(new MockHttpServletRequest(), response); + } + + private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; this.response = response; } + private StubMvcResult(RequestBuilder requestBuilder) { + this(requestBuilder.buildRequest(new MockServletContext())); + } + @Override public MockHttpServletRequest getRequest() { return this.request; From bad5bdabaa5d4a2e0d58a28c60025ea542ff0453 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 09:34:52 +0100 Subject: [PATCH 0060/1059] Improve test failure output to ease diagnosis of Travis CI failures --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 46195c883..9bbf6b06d 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,9 @@ project(':spring-restdocs') { test { jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" + testLogging { + exceptionFormat "full" + } } } From 37f9f02580a9e932530511a3ec1876b464989a52 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 10:05:42 +0100 Subject: [PATCH 0061/1059] Make the ordering of extracted links match the ordering in the payload Closes gh-63 --- .../restdocs/hypermedia/LinkExtractors.java | 17 +++++++---------- .../HypermediaDocumentationTests.java | 17 +++++------------ .../hypermedia/LinkExtractorsPayloadTests.java | 14 ++++---------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java index 806496c5d..35c0f778f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java @@ -19,13 +19,15 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -107,7 +109,7 @@ static class HalLinkExtractor extends JsonContentLinkExtractor { @Override public Map> extractLinks(Map json) { - Map> extractedLinks = new HashMap<>(); + Map> extractedLinks = new LinkedHashMap<>(); Object possibleLinks = json.get("_links"); if (possibleLinks instanceof Map) { Map links = (Map) possibleLinks; @@ -152,7 +154,7 @@ static class AtomLinkExtractor extends JsonContentLinkExtractor { @Override public Map> extractLinks(Map json) { - Map> extractedLinks = new HashMap<>(); + MultiValueMap extractedLinks = new LinkedMultiValueMap<>(); Object possibleLinks = json.get("links"); if (possibleLinks instanceof Collection) { Collection linksCollection = (Collection) possibleLinks; @@ -176,14 +178,9 @@ private static Link maybeCreateLink(Map linkMap) { } private static void maybeStoreLink(Link link, - Map> extractedLinks) { + MultiValueMap extractedLinks) { if (link != null) { - List linksForRel = extractedLinks.get(link.getRel()); - if (linksForRel == null) { - linksForRel = new ArrayList(); - extractedLinks.put(link.getRel(), linksForRel); - } - linksForRel.add(link); + extractedLinks.add(link.getRel(), link); } } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 15073949f..0eb38b2a0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -22,10 +22,6 @@ import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.junit.Rule; import org.junit.Test; @@ -33,6 +29,8 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.RestDocumentationException; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Tests for {@link HypermediaDocumentation} @@ -92,22 +90,17 @@ public void documentedLinks() throws IOException { private static class StubLinkExtractor implements LinkExtractor { - private Map> linksByRel = new HashMap>(); + private MultiValueMap linksByRel = new LinkedMultiValueMap(); @Override - public Map> extractLinks(MockHttpServletResponse response) + public MultiValueMap extractLinks(MockHttpServletResponse response) throws IOException { return this.linksByRel; } private StubLinkExtractor withLinks(Link... links) { for (Link link : links) { - List linksWithRel = this.linksByRel.get(link.getRel()); - if (linksWithRel == null) { - linksWithRel = new ArrayList(); - this.linksByRel.put(link.getRel(), linksWithRel); - } - linksWithRel.add(link); + this.linksByRel.add(link.getRel(), link); } return this; } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index eba107de3..036aa1b5d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -21,11 +21,9 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +33,8 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.FileCopyUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Parameterized tests for {@link LinkExtractors} with various payloads. @@ -97,15 +97,9 @@ public void linksInTheWrongFormat() throws IOException { } private void assertLinks(List expectedLinks, Map> actualLinks) { - Map> expectedLinksByRel = new HashMap<>(); + MultiValueMap expectedLinksByRel = new LinkedMultiValueMap<>(); for (Link expectedLink : expectedLinks) { - List expectedlinksWithRel = expectedLinksByRel.get(expectedLink - .getRel()); - if (expectedlinksWithRel == null) { - expectedlinksWithRel = new ArrayList<>(); - expectedLinksByRel.put(expectedLink.getRel(), expectedlinksWithRel); - } - expectedlinksWithRel.add(expectedLink); + expectedLinksByRel.add(expectedLink.getRel(), expectedLink); } assertEquals(expectedLinksByRel, actualLinks); } From 9749ec18eb3010e61aa2b1561cd57de8c7756271 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 09:37:11 +0100 Subject: [PATCH 0062/1059] Remove package tangle caused by RestDocumentationException --- .../hypermedia/LinkSnippetResultHandler.java | 4 ++-- .../restdocs/payload/FieldValidator.java | 4 ++-- .../SnippetGenerationException.java} | 14 +++++++------- .../hypermedia/HypermediaDocumentationTests.java | 8 ++++---- .../restdocs/payload/FieldValidatorTests.java | 6 +++--- .../payload/PayloadDocumentationTests.java | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) rename spring-restdocs/src/main/java/org/springframework/restdocs/{RestDocumentationException.java => snippet/SnippetGenerationException.java} (71%) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 0940ccfb8..a86c22656 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -24,8 +24,8 @@ import java.util.Map.Entry; import java.util.Set; -import org.springframework.restdocs.RestDocumentationException; import org.springframework.restdocs.snippet.DocumentationWriter; +import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; @@ -99,7 +99,7 @@ protected void handle(MvcResult result, DocumentationWriter writer) message += "Links with the following relations were not found in the response: " + missingRels; } - throw new RestDocumentationException(message); + throw new SnippetGenerationException(message); } Assert.isTrue(actualRels.equals(expectedRels)); 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 index e8de809fd..4404ff685 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.snippet.SnippetGenerationException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -63,7 +63,7 @@ void validate(Reader payloadReader, List fieldDescriptors) message += "Fields with the following paths were not found in the payload: " + missingFields; } - throw new RestDocumentationException(message); + throw new SnippetGenerationException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java similarity index 71% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java index f63c1f22f..027d82825 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationException.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java @@ -14,23 +14,23 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.snippet; /** - * A {@link RuntimeException} thrown to indicate a problem with a RESTful resource's - * documentation. + * A {@link RuntimeException} thrown to indicate a problem with the generation of a + * documentation snippet. * * @author Andy Wilkinson */ @SuppressWarnings("serial") -public class RestDocumentationException extends RuntimeException { +public class SnippetGenerationException extends RuntimeException { /** - * Creates a new {@code RestDocumentationException} described by the given + * Creates a new {@code SnippetGenerationException} described by the given * {@code message} - * @param message the message that describes the documentation problem + * @param message the message that describes the problem */ - public RestDocumentationException(String message) { + public SnippetGenerationException(String message) { super(message); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 0eb38b2a0..8f7df315a 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -47,7 +47,7 @@ public class HypermediaDocumentationTests { @Test public void undocumentedLink() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [foo]")); documentLinks("undocumented-link", @@ -57,7 +57,7 @@ public void undocumentedLink() throws IOException { @Test public void missingLink() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); documentLinks("undocumented-link", new StubLinkExtractor(), @@ -66,7 +66,7 @@ public void missingLink() throws IOException { @Test public void undocumentedLinkAndMissingLink() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); 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 index 2706a8324..081e3be5f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -24,7 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.snippet.SnippetGenerationException; /** * Tests for {@link FieldValidator} @@ -70,7 +70,7 @@ public void childIsDocumentedWhenParentIsDocumented() throws IOException { @Test public void missingField() throws IOException { - this.thrownException.expect(RestDocumentationException.class); + this.thrownException.expect(SnippetGenerationException.class); this.thrownException .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [y, z]")); @@ -81,7 +81,7 @@ public void missingField() throws IOException { @Test public void undocumentedField() throws IOException { - this.thrownException.expect(RestDocumentationException.class); + this.thrownException.expect(SnippetGenerationException.class); this.thrownException.expectMessage(equalTo(String .format("The following parts of the payload were not" + " documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}"))); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index fd3be61fa..38a8d38c0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -31,7 +31,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.RestDocumentationException; +import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -89,7 +89,7 @@ public void responseWithFields() throws IOException { @Test public void undocumentedRequestField() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); @@ -99,7 +99,7 @@ public void undocumentedRequestField() throws IOException { @Test public void missingRequestField() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a.b]")); @@ -110,7 +110,7 @@ public void missingRequestField() throws IOException { @Test public void undocumentedRequestFieldAndMissingRequestField() throws IOException { - this.thrown.expect(RestDocumentationException.class); + this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); From bff26b1cabdd5ea89bfadf87a26a959670b24c8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 10:18:29 +0100 Subject: [PATCH 0063/1059] Order documented links use the order of the provided descriptors --- .../restdocs/hypermedia/LinkSnippetResultHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index a86c22656..5bd8cbe7e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -17,17 +17,17 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; +import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -40,7 +40,7 @@ */ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { - private final Map descriptorsByRel = new HashMap(); + private final Map descriptorsByRel = new LinkedHashMap<>(); private final LinkExtractor extractor; From b362c493ffe536f9f3e335109739a0af577eff65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 10:21:41 +0100 Subject: [PATCH 0064/1059] Use HttpHeaders to simplify HTTP request documentation --- .../restdocs/http/HttpDocumentation.java | 2 +- .../util/DocumentableHttpServletRequest.java | 20 ++++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 6726da47f..b340c5b1f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -119,7 +119,7 @@ else if (request.isPostRequest()) { private boolean requiresFormEncodingContentType( DocumentableHttpServletRequest request) { - return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null + return request.getHeaders().getContentType() == null && request.isPostRequest() && StringUtils.hasText(request.getParameterMapAsQueryString()); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java index 3d228cab3..bf1588eef 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -22,13 +22,11 @@ import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; @@ -76,24 +74,22 @@ public boolean isPostRequest() { } /** - * Returns a Map of the request's headers. The entries are ordered based on the - * ordering of {@link HttpServletRequest#getHeaderNames()} and + * Returns the request's headers. The headers are ordered based on the ordering of + * {@link HttpServletRequest#getHeaderNames()} and * {@link HttpServletRequest#getHeaders(String)}. * - * @return the request's headers, keyed by name + * @return the request's headers * @see HttpServletRequest#getHeaderNames() * @see HttpServletRequest#getHeaders(String) */ - public Map> getHeaders() { - Map> headersByName = new LinkedHashMap>(); + public HttpHeaders getHeaders() { + HttpHeaders httpHeaders = new HttpHeaders(); for (String headerName : iterable(this.delegate.getHeaderNames())) { - List headers = new ArrayList(); - headersByName.put(headerName, headers); for (String header : iterable(this.delegate.getHeaders(headerName))) { - headers.add(header); + httpHeaders.add(headerName, header); } } - return headersByName; + return httpHeaders; } /** From b3eee08b5d6d3cc7a3ca04b4b7c00917b7d5dff9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Apr 2015 09:22:33 +0100 Subject: [PATCH 0065/1059] =?UTF-8?q?Add=20support=20for=20documenting=20a?= =?UTF-8?q?=20request=E2=80=99s=20query=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for documenting a request's query parameters. For example: mockMvc.perform( get("/issues").param("page", "2").param("limit", "20")) .andDo(documentQueryParameters("list-issues", parameterWithName("page") .description("Page number to return"), parameterWithName("limit") .description("Maximum page size"))); Closes gh-39 --- .../RestDocumentationResultHandler.java | 21 ++++ .../restdocs/request/ParameterDescriptor.java | 55 +++++++++ .../QueryParametersSnippetResultHandler.java | 105 ++++++++++++++++++ .../request/RequestDocumentation.java | 59 ++++++++++ .../request/RequestDocumentationTests.java | 103 +++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 5 + 6 files changed, 348 insertions(+) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java 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 11b555348..912d4650a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -22,6 +22,7 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; +import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import java.util.ArrayList; import java.util.List; @@ -32,6 +33,8 @@ import org.springframework.restdocs.hypermedia.LinkExtractors; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.PayloadDocumentation; +import org.springframework.restdocs.request.ParameterDescriptor; +import org.springframework.restdocs.request.RequestDocumentation; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -138,4 +141,22 @@ public RestDocumentationResultHandler withResponseFields( return this; } + /** + * Documents the parameters in the request's query string using the given + * {@code descriptors}. + *

+ * If a parameter is present in the query string but is not described by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * parameter is described but is not present in the request a failure will also occur + * when this handler is invoked. + * + * @param descriptors the parameter descriptors + * @return {@code this} + * @see RequestDocumentation#parameterWithName(String) + */ + public RestDocumentationResultHandler withQueryParameters( + ParameterDescriptor... descriptors) { + this.delegates.add(documentQueryParameters(this.outputDir, descriptors)); + return this; + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java new file mode 100644 index 000000000..63a1be58b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -0,0 +1,55 @@ +/* + * 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.request; + +/** + * A descriptor of a parameter in a query string + * + * @author Andy Wilkinson + * @see RequestDocumentation#parameterWithName + * + */ +public class ParameterDescriptor { + + private final String name; + + private String description; + + ParameterDescriptor(String name) { + this.name = name; + } + + /** + * Specifies the description of the parameter + * + * @param description The parameter's description + * @return {@code this} + */ + public ParameterDescriptor description(String description) { + this.description = description; + return this; + } + + String getName() { + return this.name; + } + + String getDescription() { + return this.description; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java new file mode 100644 index 000000000..1058b0a8c --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -0,0 +1,105 @@ +/* + * 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.request; + +import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +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.SnippetGenerationException; +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 the query + * parameters supported by a RESTful resource. + * + * @author Andy Wilkinson + */ +public class QueryParametersSnippetResultHandler extends SnippetWritingResultHandler { + + private final Map descriptorsByName = new LinkedHashMap<>(); + + protected QueryParametersSnippetResultHandler(String outputDir, + ParameterDescriptor... descriptors) { + super(outputDir, "query-parameters"); + for (ParameterDescriptor descriptor : descriptors) { + Assert.hasText(descriptor.getName()); + Assert.hasText(descriptor.getDescription()); + this.descriptorsByName.put(descriptor.getName(), descriptor); + } + } + + @Override + protected void handle(MvcResult result, DocumentationWriter writer) + throws IOException { + verifyParameterDescriptors(result); + documentParameters(writer); + } + + private void verifyParameterDescriptors(MvcResult result) { + Set actualParameters = result.getRequest().getParameterMap().keySet(); + Set expectedParameters = this.descriptorsByName.keySet(); + + Set undocumentedParameters = new HashSet(actualParameters); + undocumentedParameters.removeAll(expectedParameters); + + Set missingParameters = new HashSet(expectedParameters); + missingParameters.removeAll(actualParameters); + + if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { + String message = ""; + if (!undocumentedParameters.isEmpty()) { + message += "Query parameters with the following names were not documented: " + + undocumentedParameters; + } + if (!missingParameters.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Query parameters with the following names were not found in the request: " + + missingParameters; + } + throw new SnippetGenerationException(message); + } + + Assert.isTrue(actualParameters.equals(expectedParameters)); + } + + private void documentParameters(DocumentationWriter writer) throws IOException { + writer.table(new TableAction() { + + @Override + public void perform(TableWriter tableWriter) throws IOException { + tableWriter.headers("Parameter", "Description"); + for (Entry entry : QueryParametersSnippetResultHandler.this.descriptorsByName + .entrySet()) { + tableWriter.row(entry.getKey(), entry.getValue().getDescription()); + } + } + + }); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java new file mode 100644 index 000000000..cef76d164 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -0,0 +1,59 @@ +/* + * 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.request; + +import org.springframework.restdocs.RestDocumentationResultHandler; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; + +/** + * Static factory methods for documenting aspects of a request sent to a RESTful API. + * + * @author Andy Wilkinson + */ +public abstract class RequestDocumentation { + + private RequestDocumentation() { + + } + + /** + * Creates a {@link SnippetWritingResultHandler} that will produce a snippet + * documenting a request's query parameters + * + * @param outputDir The directory to which the snippet should be written + * @param descriptors The descriptions of the parameters in the request's query string + * @return the result handler + * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) + */ + public static SnippetWritingResultHandler documentQueryParameters(String outputDir, + ParameterDescriptor... descriptors) { + return new QueryParametersSnippetResultHandler(outputDir, descriptors); + } + + /** + * Creates a {@link ParameterDescriptor} that describes a query string parameter with + * the given {@code name}. + * + * @param name The name of the parameter + * @return a {@link ParameterDescriptor} ready for further configuration + * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) + */ + public static ParameterDescriptor parameterWithName(String name) { + return new ParameterDescriptor(name); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java new file mode 100644 index 000000000..32c794b8e --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.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.request; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +import java.io.IOException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Requests for {@link RequestDocumentation} + * + * @author Andy Wilkinson + */ +public class RequestDocumentationTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void undocumentedParameter() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not documented: [a]")); + documentQueryParameters("undocumented-parameter").handle( + result(get("/").param("a", "alpha"))); + } + + @Test + public void missingParameter() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not found in the request: [a]")); + documentQueryParameters("missing-parameter", + parameterWithName("a").description("one")).handle(result(get("/"))); + } + + @Test + public void undocumentedParameterAndMissingParameter() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not documented: [b]. Query parameters with the following" + + " names were not found in the request: [a]")); + documentQueryParameters("undocumented-parameter-missing-parameter", + parameterWithName("a").description("one")).handle( + result(get("/").param("b", "bravo"))); + } + + @Test + public void parameterSnippetFromRequestParameters() throws IOException { + this.snippet.expectQueryParameters("parameter-snippet-request-parameters") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row( + "b", "two")); + documentQueryParameters("parameter-snippet-request-parameters", + parameterWithName("a").description("one"), + parameterWithName("b").description("two")).handle( + result(get("/").param("a", "bravo").param("b", "bravo"))); + } + + @Test + public void parameterSnippetFromRequestUriQueryString() throws IOException { + this.snippet.expectQueryParameters("parameter-snippet-request-uri-query-string") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row( + "b", "two")); + documentQueryParameters("parameter-snippet-request-uri-query-string", + parameterWithName("a").description("one"), + parameterWithName("b").description("two")).handle( + result(get("/?a=alpha&b=bravo"))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 5c7f9b52f..d7ec0d71f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -142,6 +142,11 @@ public ExpectedSnippet expectHttpResponse(String name) { return this; } + public ExpectedSnippet expectQueryParameters(String name) { + expect(name, "query-parameters"); + return this; + } + private ExpectedSnippet expect(String name, String type) { this.expectedName = name; this.expectedType = type; From e5f35c98f371e7132e114fdcffca5f0c14fc40d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Apr 2015 11:12:55 +0100 Subject: [PATCH 0066/1059] Add support for post-processing a response before it is documented This commit adds support for post-processing a response before it is documented. Post-processors are provided to pretty-print JSON and XML responses, remove headers from the response, and mask the hrefs of Atom- and HAL-formatted links in JSON responses. Response post-processing is configured prior to configuring the documentation. For example, mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"), maskLinks()).andDocument("post-processed")); Closes gh-61 Closes gh-31 Closes gh-54 --- build.gradle | 4 +- .../restdocs/ResponseModifier.java | 117 ++++++++++++++ .../restdocs/RestDocumentation.java | 17 ++ .../RestDocumentationResultHandler.java | 17 +- .../ContentModifyingReponsePostProcessor.java | 96 +++++++++++ .../HeaderRemovingResponsePostProcessor.java | 132 +++++++++++++++ .../LinkMaskingResponsePostProcessor.java | 55 +++++++ .../PrettyPrintingResponsePostProcessor.java | 88 ++++++++++ .../response/ResponsePostProcessor.java | 39 +++++ .../response/ResponsePostProcessors.java | 74 +++++++++ .../restdocs/ResponseModifierTests.java | 59 +++++++ .../RestDocumentationIntegrationTests.java | 56 ++++++- ...ntModifyingResponsePostProcessorTests.java | 68 ++++++++ ...derRemovingResponsePostProcessorTests.java | 91 +++++++++++ ...LinkMaskingResponsePostProcessorTests.java | 150 ++++++++++++++++++ ...ttyPrintingResponsePostProcessorTests.java | 59 +++++++ .../restdocs/test/ExpectedSnippet.java | 60 +------ .../restdocs/test/SnippetMatchers.java | 77 ++++++++- .../restdocs/test/StubMvcResult.java | 9 ++ 19 files changed, 1196 insertions(+), 72 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java diff --git a/build.gradle b/build.gradle index 9bbf6b06d..ef09e161f 100644 --- a/build.gradle +++ b/build.gradle @@ -59,13 +59,13 @@ project(':spring-restdocs') { cleanEclipseJdt.onlyIf { false } dependencies { + compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" compile "junit:junit:$junitVersion" compile "org.springframework:spring-test:$springVersion" compile "org.springframework:spring-web:$springVersion" + compile "org.springframework:spring-webmvc:$springVersion" compile "javax.servlet:javax.servlet-api:$servletApiVersion" - compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" - testCompile "org.springframework:spring-webmvc:$springVersion" testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion" testCompile "org.mockito:mockito-core:$mockitoVersion" testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java new file mode 100644 index 000000000..40c04285e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java @@ -0,0 +1,117 @@ +/* + * 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; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.response.ResponsePostProcessor; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.ReflectionUtils; + +/** + * Modifies the response in an {@link MvcResult} by applying {@link ResponsePostProcessor + * ResponsePostProcessors} to it. + * + * @see RestDocumentation#modifyResponseTo(ResponsePostProcessor...) + * @author Andy Wilkinson + */ +public class ResponseModifier { + + private final List postProcessors; + + ResponseModifier(ResponsePostProcessor... postProcessors) { + this.postProcessors = Arrays.asList(postProcessors); + } + + /** + * Provides a {@link RestDocumentationResultHandler} that can be used to document the + * request and modified result. + * @param outputDir The directory to which the documentation will be written + * @return the result handler that will produce the documentation + */ + public RestDocumentationResultHandler andDocument(String outputDir) { + return new ResponseModifyingRestDocumentationResultHandler(outputDir); + } + + class ResponseModifyingRestDocumentationResultHandler extends + RestDocumentationResultHandler { + + public ResponseModifyingRestDocumentationResultHandler(String outputDir) { + super(outputDir); + } + + @Override + public void handle(MvcResult result) throws Exception { + super.handle(postProcessResponse(result)); + } + + MvcResult postProcessResponse(MvcResult result) throws Exception { + MockHttpServletResponse response = result.getResponse(); + for (ResponsePostProcessor postProcessor : ResponseModifier.this.postProcessors) { + response = postProcessor.postProcess(response); + } + return decorateResult(result, response); + } + + private MvcResult decorateResult(MvcResult result, + MockHttpServletResponse response) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(MvcResult.class); + enhancer.setCallback(new GetResponseMethodInterceptor(response, result)); + return (MvcResult) enhancer.create(); + } + + private class GetResponseMethodInterceptor implements MethodInterceptor { + + private final MvcResult delegate; + + private final MockHttpServletResponse response; + + private final Method getResponseMethod = findMethod("getResponse"); + + private GetResponseMethodInterceptor(MockHttpServletResponse response, + MvcResult delegate) { + this.delegate = delegate; + this.response = response; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, + MethodProxy methodProxy) throws Throwable { + if (this.getResponseMethod.equals(method)) { + return this.response; + } + return method.invoke(this.delegate, args); + } + + private Method findMethod(String methodName) { + return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod( + MvcResult.class, methodName)); + } + + } + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index 87cd79e4f..f0320e91e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -16,7 +16,10 @@ package org.springframework.restdocs; +import org.springframework.restdocs.response.ResponsePostProcessor; +import org.springframework.restdocs.response.ResponsePostProcessors; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; /** @@ -42,4 +45,18 @@ public static RestDocumentationResultHandler document(String outputDir) { return new RestDocumentationResultHandler(outputDir); } + /** + * Enables the modification of the response in a {@link MvcResult} prior to it being + * documented. The modification is performed using the given + * {@code responsePostProcessors}. + * + * @param responsePostProcessors the post-processors to use to modify the response + * @return the response modifier + * @see ResponsePostProcessors + */ + public static ResponseModifier modifyResponseTo( + ResponsePostProcessor... responsePostProcessors) { + return new ResponseModifier(responsePostProcessors); + } + } 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 912d4650a..4beea6a78 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -49,7 +49,7 @@ public class RestDocumentationResultHandler implements ResultHandler { private final String outputDir; - private List delegates; + private List delegates = new ArrayList<>(); RestDocumentationResultHandler(String outputDir) { this.outputDir = outputDir; @@ -60,13 +60,6 @@ public class RestDocumentationResultHandler implements ResultHandler { this.delegates.add(documentHttpResponse(this.outputDir)); } - @Override - public void handle(MvcResult result) throws Exception { - for (ResultHandler delegate : this.delegates) { - delegate.handle(result); - } - } - /** * Document the links in the response using the given {@code descriptors}. The links * are extracted from the response based on its content type. @@ -159,4 +152,12 @@ public RestDocumentationResultHandler withQueryParameters( this.delegates.add(documentQueryParameters(this.outputDir, descriptors)); return this; } + + @Override + public void handle(MvcResult result) throws Exception { + for (ResultHandler delegate : this.delegates) { + delegate.handle(result); + } + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java new file mode 100644 index 000000000..67bc6ddce --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -0,0 +1,96 @@ +/* + * 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.response; + +import java.lang.reflect.Method; + +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ReflectionUtils; + +/** + * A base class for {@link ResponsePostProcessor ResponsePostProcessors} that modify the + * content of the response. + * + * @author Andy Wilkinson + */ +public abstract class ContentModifyingReponsePostProcessor implements + ResponsePostProcessor { + + @Override + public MockHttpServletResponse postProcess(MockHttpServletResponse response) + throws Exception { + String modifiedContent = modifyContent(response.getContentAsString()); + + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(MockHttpServletResponse.class); + enhancer.setCallback(new ContentModifyingMethodInterceptor(modifiedContent, + response)); + + return (MockHttpServletResponse) enhancer.create(); + } + + /** + * Returns a modified version of the given {@code originalContent} + * + * @param originalContent the content to modify + * @return the modified content + * @throws Exception if a failure occurs while modifying the content + */ + protected abstract String modifyContent(String originalContent) throws Exception; + + private static class ContentModifyingMethodInterceptor implements MethodInterceptor { + + private final Method getContentAsStringMethod = findMethod("getContentAsString"); + + private final Method getContentAsByteArray = findMethod("getContentAsByteArray"); + + private final String modifiedContent; + + private final MockHttpServletResponse delegate; + + public ContentModifyingMethodInterceptor(String modifiedContent, + MockHttpServletResponse delegate) { + this.modifiedContent = modifiedContent; + this.delegate = delegate; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, + MethodProxy methodProxy) throws Throwable { + if (this.getContentAsStringMethod.equals(method)) { + return this.modifiedContent; + } + if (this.getContentAsByteArray.equals(method)) { + throw new UnsupportedOperationException( + "Following modification, the response's content should be" + + " accessed as a String"); + } + return method.invoke(this.delegate, args); + } + + private static Method findMethod(String methodName) { + return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod( + MockHttpServletResponse.class, methodName)); + } + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java new file mode 100644 index 000000000..e3b926d0b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java @@ -0,0 +1,132 @@ +/* + * 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.response; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ReflectionUtils; + +/** + * A {@link ResponsePostProcessor} that removes headers from the response + * + * @author Andy Wilkinson + */ +class HeaderRemovingResponsePostProcessor implements ResponsePostProcessor { + + private final Set headersToRemove; + + public HeaderRemovingResponsePostProcessor(String... headersToRemove) { + this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + } + + @Override + public MockHttpServletResponse postProcess(final MockHttpServletResponse response) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(MockHttpServletResponse.class); + enhancer.setCallback(new HeaderHidingMethodInterceptor(this.headersToRemove, + response)); + + return (MockHttpServletResponse) enhancer.create(); + } + + private static final class HeaderHidingMethodInterceptor implements MethodInterceptor { + + private final MockHttpServletResponse response; + + private final List interceptedMethods = Arrays.asList( + findHeaderMethod("containsHeader", String.class), + findHeaderMethod("getHeader", String.class), + findHeaderMethod("getHeaderValue", String.class), + findHeaderMethod("getHeaders", String.class), + findHeaderMethod("getHeaderValues", String.class)); + + private final Method getHeaderNamesMethod = findHeaderMethod("getHeaderNames"); + + private final Set hiddenHeaders; + + private HeaderHidingMethodInterceptor(Set hiddenHeaders, + MockHttpServletResponse response) { + this.hiddenHeaders = hiddenHeaders; + this.response = response; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, + MethodProxy methodProxy) throws Throwable { + if (this.getHeaderNamesMethod.equals(method)) { + List headerNames = new ArrayList<>(); + for (String candidate : this.response.getHeaderNames()) { + if (!isHiddenHeader(candidate)) { + headerNames.add(candidate); + } + } + return headerNames; + } + if (this.interceptedMethods.contains(method) && isHiddenHeader(args)) { + if (method.getReturnType().equals(boolean.class)) { + return false; + } + else if (Collection.class.isAssignableFrom(method.getReturnType())) { + return Collections.emptyList(); + } + else { + return null; + } + } + + return method.invoke(this.response, args); + } + + private boolean isHiddenHeader(Object[] args) { + if (args.length == 1 && args[0] instanceof String) { + return isHiddenHeader((String) args[0]); + } + return false; + } + + private boolean isHiddenHeader(String headerName) { + for (String hiddenHeader : this.hiddenHeaders) { + if (hiddenHeader.equalsIgnoreCase(headerName)) { + return true; + } + } + return false; + } + + private static Method findHeaderMethod(String methodName, Class... args) { + Method candidate = ReflectionUtils.findMethod(MockHttpServletResponse.class, + methodName, args); + if (candidate.isBridge()) { + return BridgeMethodResolver.findBridgedMethod(candidate); + } + return candidate; + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java new file mode 100644 index 000000000..6ad58e4cb --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java @@ -0,0 +1,55 @@ +/* + * 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.response; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LinkMaskingResponsePostProcessor extends ContentModifyingReponsePostProcessor { + + private static final String DEFAULT_MASK = "..."; + + private static final Pattern LINK_HREF = Pattern.compile( + "\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); + + private final String mask; + + LinkMaskingResponsePostProcessor() { + this(DEFAULT_MASK); + } + + LinkMaskingResponsePostProcessor(String mask) { + this.mask = mask; + } + + @Override + protected String modifyContent(String originalContent) { + Matcher matcher = LINK_HREF.matcher(originalContent); + StringBuilder buffer = new StringBuilder(); + int previous = 0; + while (matcher.find()) { + buffer.append(originalContent.substring(previous, matcher.start(1))); + buffer.append(this.mask); + previous = matcher.end(1); + } + if (previous < originalContent.length()) { + buffer.append(originalContent.substring(previous)); + } + return buffer.toString(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java new file mode 100644 index 000000000..41bd7f5bf --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.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.response; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +class PrettyPrintingResponsePostProcessor extends ContentModifyingReponsePostProcessor { + + private static final List prettyPrinters = Arrays.asList( + new JsonPrettyPrinter(), new XmlPrettyPrinter()); + + @Override + protected String modifyContent(String originalContent) { + if (StringUtils.hasText(originalContent)) { + for (PrettyPrinter prettyPrinter : prettyPrinters) { + try { + return prettyPrinter.prettyPrint(originalContent); + } + catch (Exception ex) { + // Continue + } + } + } + return originalContent; + } + + private interface PrettyPrinter { + + String prettyPrint(String string) throws Exception; + + } + + private static final class XmlPrettyPrinter implements PrettyPrinter { + + @Override + public String prettyPrint(String original) throws Exception { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", + "4"); + transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); + StringWriter transformed = new StringWriter(); + transformer.transform(new StreamSource(new StringReader(original)), + new StreamResult(transformed)); + return transformed.toString(); + } + } + + private static final class JsonPrettyPrinter implements PrettyPrinter { + + @Override + public String prettyPrint(String original) throws IOException { + ObjectMapper objectMapper = new ObjectMapper().configure( + SerializationFeature.INDENT_OUTPUT, true); + return objectMapper.writeValueAsString(objectMapper.readTree(original)); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java new file mode 100644 index 000000000..c685626f7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java @@ -0,0 +1,39 @@ +/* + * 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.response; + +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A {@code ResponsePostProcessor} is used to modify the response received from a MockMvc + * call prior to the response being documented. + * + * @author Andy Wilkinson + */ +public interface ResponsePostProcessor { + + /** + * Post-processes the given {@code response}, returning a, possibly new, + * {@link MockHttpServletResponse} that should now be used. + * + * @param response The response to post-process + * @return The result of the post-processing + * @throws Exception if a failure occurs during the post-processing + */ + MockHttpServletResponse postProcess(MockHttpServletResponse response) + throws Exception; +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java new file mode 100644 index 000000000..b04128a9e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java @@ -0,0 +1,74 @@ +/* + * 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.response; + +/** + * Static factory methods for accessing various {@link ResponsePostProcessor + * ResponsePostProcessors}. + * + * @author Andy Wilkinson + */ +public abstract class ResponsePostProcessors { + + private ResponsePostProcessors() { + + } + + /** + * Returns a {@link ResponsePostProcessor} that will pretty print the content of the + * response. + * + * @return the response post-processor + */ + public static ResponsePostProcessor prettyPrintContent() { + return new PrettyPrintingResponsePostProcessor(); + } + + /** + * Returns a {@link ResponsePostProcessor} that will remove the headers with the given + * {@code headerNames} from the response. + * + * @param headerNames the name of the headers to remove + * @return the response post-processor + */ + public static ResponsePostProcessor removeHeaders(String... headerNames) { + return new HeaderRemovingResponsePostProcessor(headerNames); + } + + /** + * Returns a {@link ResponsePostProcessor} that will update the content of the + * response to mask any links that it contains. Each link is masked my replacing its + * {@code href} with {@code ...}. + * + * @return the response post-processor + */ + public static ResponsePostProcessor maskLinks() { + return new LinkMaskingResponsePostProcessor(); + } + + /** + * Returns a {@link ResponsePostProcessor} that will update the content of the + * response to mask any links that it contains. Each link is masked my replacing its + * {@code href} with the given {@code mask}. + * + * @param mask the mask to apply + * @return the response post-processor + */ + public static ResponsePostProcessor maskLinksWith(String mask) { + return new LinkMaskingResponsePostProcessor(mask); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java new file mode 100644 index 000000000..63a3862c8 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java @@ -0,0 +1,59 @@ +/* + * 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; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.ResponseModifier.ResponseModifyingRestDocumentationResultHandler; +import org.springframework.restdocs.response.ResponsePostProcessor; + +/** + * Tests for {@link ResponseModifier} + * + * @author Andy Wilkinson + * + */ +public class ResponseModifierTests { + + @Test + public void postProcessorsAreApplied() throws Exception { + ResponsePostProcessor first = mock(ResponsePostProcessor.class); + ResponsePostProcessor second = mock(ResponsePostProcessor.class); + + MockHttpServletResponse original = new MockHttpServletResponse(); + MockHttpServletResponse afterFirst = new MockHttpServletResponse(); + MockHttpServletResponse afterSecond = new MockHttpServletResponse(); + + given(first.postProcess(original)).willReturn(afterFirst); + given(second.postProcess(afterFirst)).willReturn(afterSecond); + + RestDocumentationResultHandler resultHandler = new ResponseModifier(first, second) + .andDocument("test"); + assertThat( + afterSecond, + is(equalTo(((ResponseModifyingRestDocumentationResultHandler) resultHandler) + .postProcessResponse(result(original)).getResponse()))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 0f51b4834..646623b30 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -16,12 +16,21 @@ package org.springframework.restdocs; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; +import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; +import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; +import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.SnippetMatchers.snippet; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.File; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -32,9 +41,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.restdocs.RestDocumentationIntegrationTests.TestConfiguration; import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.restdocs.hypermedia.Link; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -122,7 +135,39 @@ public void multiStep() throws Exception { assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void postProcessedResponse() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("original")); + + assertThat( + new File("build/generated-snippets/original/http-response.adoc"), + is(snippet().withContents( + httpResponse(HttpStatus.OK) + .header("a", "alpha") + .header("Content-Type", "application/json") + .content( + "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + + "\"href\":\"href\"}]}")))); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"), + maskLinks()).andDocument("post-processed")); + + assertThat( + new File("build/generated-snippets/post-processed/http-response.adoc"), + is(snippet().withContents( + httpResponse(HttpStatus.OK).header("Content-Type", + "application/json").content( + String.format("{%n \"a\" : \"alpha\",%n \"links\" :" + + " [ {%n \"rel\" : \"rel\",%n \"href\" :" + + " \"...\"%n } ]%n}"))))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { @@ -146,12 +191,15 @@ public TestController testController() { static class TestController { @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - public Map foo() { - Map response = new HashMap(); + public ResponseEntity> foo() { + Map response = new HashMap<>(); response.put("a", "alpha"); - return response; + response.put("links", Arrays.asList(new Link("rel", "href"))); + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "alpha"); + return new ResponseEntity>(response, headers, + HttpStatus.OK); } - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java new file mode 100644 index 000000000..dd07830a8 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java @@ -0,0 +1,68 @@ +/* + * 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.response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.mock.web.MockHttpServletResponse; + +public class ContentModifyingResponsePostProcessorTests { + + private final MockHttpServletResponse original = new MockHttpServletResponse(); + + private final ContentModifyingReponsePostProcessor postProcessor = new TestContentModifyingResponsePostProcessor(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void contentCanBeModified() throws Exception { + MockHttpServletResponse modified = this.postProcessor.postProcess(this.original); + assertThat(modified.getContentAsString(), is(equalTo("modified"))); + } + + @Test + public void nonContentMethodsAreDelegated() throws Exception { + this.original.addHeader("a", "alpha"); + MockHttpServletResponse modified = this.postProcessor.postProcess(this.original); + assertThat(modified.getHeader("a"), is(equalTo("alpha"))); + } + + @Test + public void getContentAsByteArrayIsUnsupported() throws Exception { + this.thrown.expect(UnsupportedOperationException.class); + this.thrown.expectMessage(equalTo("Following modification, the response's" + + " content should be accessed as a String")); + this.postProcessor.postProcess(this.original).getContentAsByteArray(); + } + + private static final class TestContentModifyingResponsePostProcessor extends + ContentModifyingReponsePostProcessor { + + @Override + protected String modifyContent(String originalContent) throws Exception { + return "modified"; + } + + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java new file mode 100644 index 000000000..1891eadc0 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java @@ -0,0 +1,91 @@ +/* + * 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.response; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Tests for {@link HeaderRemovingResponsePostProcessor}. + * + * @author Andy Wilkinson + * + */ +public class HeaderRemovingResponsePostProcessorTests { + + private final MockHttpServletResponse response = new MockHttpServletResponse(); + + @Before + public void configureResponse() { + this.response.addHeader("a", "alpha"); + this.response.addHeader("b", "bravo"); + } + + @Test + public void containsHeaderHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.containsHeader("a"), is(false)); + assertThat(response.containsHeader("b"), is(true)); + } + + @Test + public void getHeaderNamesHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.getHeaderNames(), contains("b")); + } + + @Test + public void getHeaderHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.getHeader("a"), is(nullValue())); + assertThat(response.getHeader("b"), is("bravo")); + } + + @Test + public void getHeadersHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.getHeaders("a"), is(empty())); + assertThat(response.getHeaders("b"), contains("bravo")); + } + + @Test + public void getHeaderValueHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.getHeaderValue("a"), is(nullValue())); + assertThat(response.getHeaderValue("b"), is((Object) "bravo")); + } + + @Test + public void getHeaderValuesHonoursRemovedHeaders() { + MockHttpServletResponse response = removeHeaders("a"); + assertThat(response.getHeaderValues("a"), is(empty())); + assertThat(response.getHeaderValues("b"), contains((Object) "bravo")); + } + + private MockHttpServletResponse removeHeaders(String... headerNames) { + return new HeaderRemovingResponsePostProcessor(headerNames) + .postProcess(this.response); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java new file mode 100644 index 000000000..ddc66d454 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java @@ -0,0 +1,150 @@ +/* + * 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.response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.restdocs.hypermedia.Link; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +public class LinkMaskingResponsePostProcessorTests { + + private final LinkMaskingResponsePostProcessor postProcessor = new LinkMaskingResponsePostProcessor(); + + private final Link[] links = new Link[] { new Link("a", "alpha"), + new Link("b", "bravo") }; + + private final Link[] maskedLinks = new Link[] { new Link("a", "..."), + new Link("b", "...") }; + + @Test + public void halLinksAreMasked() throws Exception { + assertThat(this.postProcessor.modifyContent(halPayloadWithLinks(this.links)), + is(equalTo(halPayloadWithLinks(this.maskedLinks)))); + } + + @Test + public void formattedHalLinksAreMasked() throws Exception { + assertThat( + this.postProcessor + .modifyContent(formattedHalPayloadWithLinks(this.links)), + is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); + } + + @Test + public void atomLinksAreMasked() throws Exception { + assertThat(this.postProcessor.modifyContent(atomPayloadWithLinks(this.links)), + is(equalTo(atomPayloadWithLinks(this.maskedLinks)))); + } + + @Test + public void formattedAtomLinksAreMasked() throws Exception { + assertThat( + this.postProcessor + .modifyContent(formattedAtomPayloadWithLinks(this.links)), + is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); + } + + @Test + public void maskCanBeCustomized() throws Exception { + assertThat( + new LinkMaskingResponsePostProcessor("custom") + .modifyContent(formattedAtomPayloadWithLinks(this.links)), + is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), + new Link("b", "custom"))))); + } + + private String atomPayloadWithLinks(Link... links) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(createAtomPayload(links)); + } + + private String formattedAtomPayloadWithLinks(Link... links) + throws JsonProcessingException { + return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) + .writeValueAsString(createAtomPayload(links)); + } + + private AtomPayload createAtomPayload(Link... links) { + AtomPayload payload = new AtomPayload(); + payload.setLinks(Arrays.asList(links)); + return payload; + } + + private String halPayloadWithLinks(Link... links) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(createHalPayload(links)); + } + + private String formattedHalPayloadWithLinks(Link... links) + throws JsonProcessingException { + return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) + .writeValueAsString(createHalPayload(links)); + } + + private HalPayload createHalPayload(Link... links) { + HalPayload payload = new HalPayload(); + Map linksMap = new LinkedHashMap<>(); + for (Link link : links) { + Map linkMap = new HashMap<>(); + linkMap.put("href", link.getHref()); + linksMap.put(link.getRel(), linkMap); + } + payload.setLinks(linksMap); + return payload; + } + + static final class AtomPayload { + + private List links; + + public void setLinks(List links) { + this.links = links; + } + + public List getLinks() { + return this.links; + } + + } + + static final class HalPayload { + + private Map links; + + @JsonProperty("_links") + public Map getLinks() { + return this.links; + } + + public void setLinks(Map links) { + this.links = links; + } + + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java new file mode 100644 index 000000000..5a5b45775 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java @@ -0,0 +1,59 @@ +/* + * 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.response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +/** + * Tests for {@link PrettyPrintingResponsePostProcessor} + * + * @author Andy Wilkinson + * + */ +public class PrettyPrintingResponsePostProcessorTests { + + @Test + public void prettyPrintJson() throws Exception { + assertThat(new PrettyPrintingResponsePostProcessor().modifyContent("{\"a\":5}"), + equalTo(String.format("{%n \"a\" : 5%n}"))); + } + + @Test + public void prettyPrintXml() throws Exception { + assertThat( + new PrettyPrintingResponsePostProcessor() + .modifyContent(""), + equalTo(String.format("%n" + + "%n %n%n"))); + } + + @Test + public void empytContentIsHandledGracefully() throws Exception { + assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(""), + equalTo("")); + } + + @Test + public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { + String content = "abcdefg"; + assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(content), + equalTo(content)); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index d7ec0d71f..1bc2dda0b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -16,26 +16,18 @@ package org.springframework.restdocs.test; -import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; import org.hamcrest.Matcher; -import org.hamcrest.collection.IsIterableContainingInAnyOrder; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; +import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; /** * The {@code ExpectedSnippet} rule is used to verify that a @@ -49,7 +41,7 @@ public class ExpectedSnippet implements TestRule { private String expectedType; - private Matcher expectedContents; + private SnippetMatcher snippet = SnippetMatchers.snippet(); private File outputDir; @@ -104,11 +96,7 @@ private void verifySnippet() throws IOException { if (this.outputDir != null && this.expectedName != null) { File snippetDir = new File(this.outputDir, this.expectedName); File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); - assertTrue("The file " + snippetFile + " does not exist or is not a file", - snippetFile.isFile()); - if (this.expectedContents != null) { - assertThat(read(snippetFile), this.expectedContents); - } + assertThat(snippetFile, is(this.snippet)); } } @@ -154,45 +142,7 @@ private ExpectedSnippet expect(String name, String type) { } public void withContents(Matcher matcher) { - this.expectedContents = matcher; - } - - private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString(new FileReader(snippetFile)); - } - - public Matcher> asciidoctorTableWith(String[] header, - String[]... rows) { - Collection> matchers = new ArrayList>(); - for (String headerItem : header) { - matchers.add(equalTo(headerItem)); - } - - for (String[] row : rows) { - for (String rowItem : row) { - matchers.add(equalTo(rowItem)); - } - } - - matchers.add(equalTo("|===")); - matchers.add(equalTo("")); - - return new IsIterableContainingInAnyOrder(matchers); - } - - public String[] header(String... columns) { - String header = "|" - + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); - return new String[] { "", "|===", header, "" }; - } - - public String[] row(String... entries) { - List lines = new ArrayList(); - for (String entry : entries) { - lines.add("|" + entry); - } - lines.add(""); - return lines.toArray(new String[lines.size()]); + this.snippet.withContents(matcher); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 801ad07f1..a8cdb4d05 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -16,6 +16,9 @@ package org.springframework.restdocs.test; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; @@ -25,6 +28,7 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.springframework.http.HttpStatus; +import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -35,6 +39,10 @@ */ public class SnippetMatchers { + public static SnippetMatcher snippet() { + return new SnippetMatcher(); + } + public static AsciidoctorTableMatcher tableWithHeader(String... headers) { return new AsciidoctorTableMatcher(headers); } @@ -52,7 +60,8 @@ public static AsciidoctorCodeBlockMatcher codeBlock(String language) { return new AsciidoctorCodeBlockMatcher(language); } - private static abstract class AbstractSnippetMatcher extends BaseMatcher { + private static abstract class AbstractSnippetContentMatcher extends + BaseMatcher { private List lines = new ArrayList(); @@ -99,7 +108,7 @@ private String getLinesAsString() { } public static class AsciidoctorCodeBlockMatcher> - extends AbstractSnippetMatcher { + extends AbstractSnippetContentMatcher { protected AsciidoctorCodeBlockMatcher(String language) { this.addLine(""); @@ -152,7 +161,7 @@ public HttpRequestMatcher(RequestMethod requestMethod, String uri) { } - public static class AsciidoctorTableMatcher extends AbstractSnippetMatcher { + public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String... columns) { this.addLine(""); @@ -174,4 +183,66 @@ public AsciidoctorTableMatcher row(String... entries) { return this; } } + + public static class SnippetMatcher extends BaseMatcher { + + private Matcher expectedContents; + + @Override + public boolean matches(Object item) { + if (snippetFileExists(item)) { + if (this.expectedContents != null) { + try { + return this.expectedContents.matches(read((File) item)); + } + catch (IOException e) { + return false; + } + } + return true; + } + return false; + } + + private boolean snippetFileExists(Object item) { + return item instanceof File && ((File) item).isFile(); + } + + private String read(File snippetFile) throws IOException { + return FileCopyUtils.copyToString(new FileReader(snippetFile)); + } + + @Override + public void describeMismatch(Object item, Description description) { + if (!snippetFileExists(item)) { + description.appendText("The file " + item + " does not exist"); + } + else if (this.expectedContents != null) { + try { + this.expectedContents + .describeMismatch(read((File) item), description); + } + catch (IOException e) { + description.appendText("The contents of " + item + + " cound not be read"); + } + } + } + + @Override + public void describeTo(Description description) { + if (this.expectedContents != null) { + this.expectedContents.describeTo(description); + } + else { + description.appendText("Asciidoctor snippet"); + } + } + + public SnippetMatcher withContents(Matcher matcher) { + this.expectedContents = matcher; + return this; + } + + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index ab11f19d0..1d4d46513 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -45,6 +45,11 @@ public static StubMvcResult result(RequestBuilder requestBuilder) { return new StubMvcResult(requestBuilder); } + public static StubMvcResult result(RequestBuilder requestBuilder, + MockHttpServletResponse response) { + return new StubMvcResult(requestBuilder, response); + } + public static StubMvcResult result(MockHttpServletRequest request) { return new StubMvcResult(request); } @@ -65,6 +70,10 @@ private StubMvcResult(MockHttpServletResponse response) { this(new MockHttpServletRequest(), response); } + private StubMvcResult(RequestBuilder requestBuilder, MockHttpServletResponse response) { + this(requestBuilder.buildRequest(new MockServletContext()), response); + } + private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; this.response = response; From ca375c919c85f026a75c07383c338c8ba81b11d9 Mon Sep 17 00:00:00 2001 From: Jonathan Pearlin Date: Tue, 28 Apr 2015 16:21:38 -0700 Subject: [PATCH 0067/1059] Improve support for documenting PUT requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, no special consideration was given to PUT requests. This meant that curl and HTTP request snippets would not include the request’s parameters/query string and that HTTP request snippets would not set the content type to application/x-www-form-urlencoded. This commit addresses these limitations by updating both HttpDocumentation and CurlDocumentation to treat PUT requests in the same way as POST requests. Closes gh-62 --- .../restdocs/curl/CurlDocumentation.java | 3 +- .../restdocs/http/HttpDocumentation.java | 5 +-- .../util/DocumentableHttpServletRequest.java | 12 ++++++- .../restdocs/curl/CurlDocumentationTests.java | 32 +++++++++++++++++++ .../restdocs/http/HttpDocumentationTests.java | 24 ++++++++++++++ 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 732c3ad5c..05199d440 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -34,6 +34,7 @@ * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda + * @author Jonathan Pearlin */ public abstract class CurlDocumentation { @@ -114,7 +115,7 @@ public void perform() throws IOException { this.writer .print(String.format(" -d '%s'", request.getContentAsString())); } - else if (request.isPostRequest()) { + else if (request.isPostRequest() || request.isPutRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { this.writer.print(String.format(" -d '%s'", queryString)); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index b340c5b1f..0e1b9fc9f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -34,6 +34,7 @@ * Static factory methods for documenting a RESTful API's HTTP requests. * * @author Andy Wilkinson + * @author Jonathan Pearlin */ public abstract class HttpDocumentation { @@ -109,7 +110,7 @@ public void perform() throws IOException { if (request.getContentLength() > 0) { this.writer.println(request.getContentAsString()); } - else if (request.isPostRequest()) { + else if (request.isPostRequest() || request.isPutRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { this.writer.println(queryString); @@ -120,7 +121,7 @@ else if (request.isPostRequest()) { private boolean requiresFormEncodingContentType( DocumentableHttpServletRequest request) { return request.getHeaders().getContentType() == null - && request.isPostRequest() + && (request.isPostRequest() || request.isPutRequest()) && StringUtils.hasText(request.getParameterMapAsQueryString()); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java index bf1588eef..a4779722d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -37,7 +37,7 @@ * to help in the documentation of the request. * * @author Andy Wilkinson - * + * @author Jonathan Pearlin */ public class DocumentableHttpServletRequest { @@ -73,6 +73,16 @@ public boolean isPostRequest() { return RequestMethod.POST == RequestMethod.valueOf(this.delegate.getMethod()); } + /** + * Whether or not this request is a {@code PUT} request. + * + * @return {@code true} if it is a {@code PUT} request, otherwise {@code false} + * @see HttpServletRequest#getMethod() + */ + public boolean isPutRequest() { + return RequestMethod.PUT == RequestMethod.valueOf(this.delegate.getMethod()); + } + /** * Returns the request's headers. The headers are ordered based on the ordering of * {@link HttpServletRequest#getHeaderNames()} and diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index e984e8302..dfda7b681 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -21,6 +21,7 @@ import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import java.io.IOException; @@ -36,6 +37,7 @@ * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda + * @author Jonathan Pearlin */ public class CurlDocumentationTests { @@ -134,6 +136,36 @@ public void postRequestWithUrlEncodedParameter() throws IOException { result(post("/foo").param("k1", "a&b"))); } + @Test + public void putRequestWithOneParameter() throws IOException { + this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + documentCurlRequest("put-request-with-one-parameter").handle( + result(put("/foo").param("k1", "v1"))); + } + + @Test + public void putRequestWithMultipleParameters() throws IOException { + this.snippet.expectCurlRequest("put-request-with-multiple-parameters") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X PUT" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + documentCurlRequest("put-request-with-multiple-parameters").handle( + result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); + } + + @Test + public void putRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter") + .withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + documentCurlRequest("put-request-with-url-encoded-parameter").handle( + result(put("/foo").param("k1", "a&b"))); + } + @Test public void requestWithHeaders() throws IOException { this.snippet.expectCurlRequest("request-with-headers").withContents( diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index 4211c909c..a1e305627 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -25,8 +25,10 @@ import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; import java.io.IOException; @@ -40,6 +42,7 @@ * Tests for {@link HttpDocumentation} * * @author Andy Wilkinson + * @author Jonathan Pearlin */ public class HttpDocumentationTests { @@ -94,6 +97,27 @@ public void postRequestWithParameter() throws IOException { result(post("/foo").param("b&r", "baz").param("a", "alpha"))); } + @Test + public void putRequestWithContent() throws IOException { + this.snippet.expectHttpRequest("put-request-with-content").withContents( + httpRequest(PUT, "/foo") // + .content("Hello, world")); + + documentHttpRequest("put-request-with-content").handle( + result(put("/foo").content("Hello, world"))); + } + + @Test + public void putRequestWithParameter() throws IOException { + this.snippet.expectHttpRequest("put-request-with-parameter").withContents( + httpRequest(PUT, "/foo") // + .header("Content-Type", "application/x-www-form-urlencoded") // + .content("b%26r=baz&a=alpha")); + + documentHttpRequest("put-request-with-parameter").handle( + result(put("/foo").param("b&r", "baz").param("a", "alpha"))); + } + @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); From 84fe379f64bd1e479d70275a89c55a6eec7f2e83 Mon Sep 17 00:00:00 2001 From: Dewet Diener Date: Thu, 30 Apr 2015 16:34:31 +0100 Subject: [PATCH 0068/1059] Add a response post processor that performs generic pattern replacement This commit introduces a new ResponsePostProcessor, PatternReplacingResponsePostProcessor, that modifies the content of the response by replacing occurrences of a regular expression with a configurable replacement. The existing LinkMaskingResponsePostProcessor, which was already performing pattern replacement, has been updated to subclass PatternReplacingResponsePostProcessor. Closes gh-65 --- .../LinkMaskingResponsePostProcessor.java | 32 ++++------- ...PatternReplacingResponsePostProcessor.java | 56 +++++++++++++++++++ .../response/ResponsePostProcessors.java | 17 ++++++ .../RestDocumentationIntegrationTests.java | 9 ++- ...rnReplacingResponsePostProcessorTests.java | 47 ++++++++++++++++ 5 files changed, 137 insertions(+), 24 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java index 6ad58e4cb..8396b1633 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java @@ -16,40 +16,28 @@ package org.springframework.restdocs.response; -import java.util.regex.Matcher; import java.util.regex.Pattern; -class LinkMaskingResponsePostProcessor extends ContentModifyingReponsePostProcessor { +/** + * A {@link ResponsePostProcessor} that modifies the content of a hypermedia response to + * mask the hrefs of any links. + * + * @author Andy Wilkinson + * @author Dewet Diener + */ +class LinkMaskingResponsePostProcessor extends PatternReplacingResponsePostProcessor { private static final String DEFAULT_MASK = "..."; private static final Pattern LINK_HREF = Pattern.compile( "\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); - private final String mask; - LinkMaskingResponsePostProcessor() { - this(DEFAULT_MASK); + super(LINK_HREF, DEFAULT_MASK); } LinkMaskingResponsePostProcessor(String mask) { - this.mask = mask; - } - - @Override - protected String modifyContent(String originalContent) { - Matcher matcher = LINK_HREF.matcher(originalContent); - StringBuilder buffer = new StringBuilder(); - int previous = 0; - while (matcher.find()) { - buffer.append(originalContent.substring(previous, matcher.start(1))); - buffer.append(this.mask); - previous = matcher.end(1); - } - if (previous < originalContent.length()) { - buffer.append(originalContent.substring(previous)); - } - return buffer.toString(); + super(LINK_HREF, mask); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java new file mode 100644 index 000000000..ce6403416 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java @@ -0,0 +1,56 @@ +/* + * 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.response; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link ResponsePostProcessor} that modifies the content of the response by replacing + * occurrences of a regular expression {@link Pattern}. + * + * @author Andy Wilkinson + * @author Dewet Diener + */ +class PatternReplacingResponsePostProcessor extends ContentModifyingReponsePostProcessor { + + private final Pattern pattern; + + private final String replacement; + + PatternReplacingResponsePostProcessor(Pattern pattern, String replacement) { + this.pattern = pattern; + this.replacement = replacement; + } + + @Override + protected String modifyContent(String originalContent) { + Matcher matcher = this.pattern.matcher(originalContent); + StringBuilder buffer = new StringBuilder(); + int previous = 0; + while (matcher.find()) { + buffer.append(originalContent.substring(previous, matcher.start(1))); + buffer.append(this.replacement); + previous = matcher.end(1); + } + if (previous < originalContent.length()) { + buffer.append(originalContent.substring(previous)); + } + return buffer.toString(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java index b04128a9e..6c1f22952 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java @@ -16,11 +16,14 @@ package org.springframework.restdocs.response; +import java.util.regex.Pattern; + /** * Static factory methods for accessing various {@link ResponsePostProcessor * ResponsePostProcessors}. * * @author Andy Wilkinson + * @author Dewet Diener */ public abstract class ResponsePostProcessors { @@ -71,4 +74,18 @@ public static ResponsePostProcessor maskLinks() { public static ResponsePostProcessor maskLinksWith(String mask) { return new LinkMaskingResponsePostProcessor(mask); } + + /** + * Returns a {@link ResponsePostProcessor} that will update the content of the + * response by replacing any occurrences of the given {@code pattern} with the given + * {@code replacement}. + * + * @param pattern the pattern to match + * @param replacement the replacement to apply + * @return the response post-processor + */ + public static ResponsePostProcessor replacePattern(Pattern pattern, String replacement) { + return new PatternReplacingResponsePostProcessor(pattern, replacement); + } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 646623b30..a7dc2b94f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -24,6 +24,7 @@ import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; +import static org.springframework.restdocs.response.ResponsePostProcessors.replacePattern; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -33,6 +34,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.regex.Pattern; import org.junit.After; import org.junit.Before; @@ -64,6 +66,7 @@ * Integration tests for Spring REST Docs * * @author Andy Wilkinson + * @author Dewet Diener */ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @@ -155,17 +158,19 @@ public void postProcessedResponse() throws Exception { "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + "\"href\":\"href\"}]}")))); + Pattern pattern = Pattern.compile("(\"alpha\")"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"), - maskLinks()).andDocument("post-processed")); + replacePattern(pattern, "\"<>\""), maskLinks()) + .andDocument("post-processed")); assertThat( new File("build/generated-snippets/post-processed/http-response.adoc"), is(snippet().withContents( httpResponse(HttpStatus.OK).header("Content-Type", "application/json").content( - String.format("{%n \"a\" : \"alpha\",%n \"links\" :" + String.format("{%n \"a\" : \"<>\",%n \"links\" :" + " [ {%n \"rel\" : \"rel\",%n \"href\" :" + " \"...\"%n } ]%n}"))))); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java new file mode 100644 index 000000000..1074ea352 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java @@ -0,0 +1,47 @@ +/* + * 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.response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * Tests for {@link PatternReplacingResponsePostProcessor}. + * + * @author Dewet Diener + */ +public class PatternReplacingResponsePostProcessorTests { + + @Test + public void patternsAreReplaced() throws Exception { + Pattern pattern = Pattern.compile( + "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", + Pattern.CASE_INSENSITIVE); + PatternReplacingResponsePostProcessor postProcessor = new PatternReplacingResponsePostProcessor( + pattern, "<>"); + assertThat( + postProcessor + .modifyContent("{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}"), + is(equalTo("{\"id\" : \"<>\"}"))); + } + +} From 90bbb5ab9ef18230f90bac791562542f9ae1918b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 May 2015 12:04:33 +0100 Subject: [PATCH 0069/1059] Update samples to make them compatible with Java 7 The main project supports Java 7 or later, but, prior to this commit, the Spring HATEOAS sample required Java 8. This commit updates updates the Spring HATEOAS sample to make it compatible with Java 7 and updates the build configuration of both samples to specify Java 7. Closes gh-51 --- .travis.yml | 7 ++-- .../.settings/org.eclipse.jdt.core.prefs | 6 +-- .../rest-notes-spring-data-rest/build.gradle | 3 ++ samples/rest-notes-spring-data-rest/pom.xml | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 6 +-- .../rest-notes-spring-hateoas/build.gradle | 3 ++ samples/rest-notes-spring-hateoas/pom.xml | 2 +- .../com/example/notes/NoteRepository.java | 5 +-- .../com/example/notes/NotesController.java | 41 +++++++++++-------- .../java/com/example/notes/TagRepository.java | 6 +-- .../com/example/notes/TagsController.java | 21 ++++++---- 11 files changed, 57 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index 43bb5feb1..0477e7d0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: java +jdk: + - oraclejdk7 script: - - jdk_switcher use oraclejdk7 - - "./gradlew build" - - jdk_switcher use oraclejdk8 - - "./gradlew buildSamples" \ No newline at end of file + - "./gradlew build buildSamples" \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs index 03628c92c..83412a90a 100644 --- a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs +++ b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs @@ -1,14 +1,14 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.compliance=1.7 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.source=1.7 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle index 41b3e6564..0acf3af59 100644 --- a/samples/rest-notes-spring-data-rest/build.gradle +++ b/samples/rest-notes-spring-data-rest/build.gradle @@ -23,6 +23,9 @@ repositories { group = 'com.example' +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-rest' compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 5346f7699..a12bcf064 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -17,7 +17,7 @@ UTF-8 - 1.8 + 1.7 diff --git a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs index 03628c92c..83412a90a 100644 --- a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs +++ b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs @@ -1,14 +1,14 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.compliance=1.7 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.source=1.7 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index fe5b449c9..d8b78d536 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -23,6 +23,9 @@ repositories { group = 'com.example' +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + dependencies { compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml index 1c9bfbbaa..7484e4852 100644 --- a/samples/rest-notes-spring-hateoas/pom.xml +++ b/samples/rest-notes-spring-hateoas/pom.xml @@ -17,7 +17,7 @@ UTF-8 - 1.8 + 1.7 diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java index 01e41b572..87c0a72f3 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -18,13 +18,12 @@ import java.util.Collection; import java.util.List; -import java.util.Optional; import org.springframework.data.repository.CrudRepository; public interface NoteRepository extends CrudRepository { - Optional findById(long id); + Note findById(long id); List findByTagsIn(Collection tags); } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java index 2ff6c5f15..0eccb0e24 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -19,8 +19,8 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import java.net.URI; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.Resource; @@ -92,25 +92,19 @@ void delete(@PathVariable("id") long id) { @RequestMapping(value = "/{id}", method = RequestMethod.GET) Resource note(@PathVariable("id") long id) { - Note note = this.noteRepository.findById(id).orElseThrow( - () -> new ResourceDoesNotExistException()); - return this.noteResourceAssembler.toResource(note); + return this.noteResourceAssembler.toResource(findNoteById(id)); } @RequestMapping(value = "/{id}/tags", method = RequestMethod.GET) ResourceSupport noteTags(@PathVariable("id") long id) { return new NestedContentResource( - this.tagResourceAssembler.toResources(this.noteRepository - .findById(id) - .orElseThrow( - () -> new ResourceDoesNotExistException()).getTags())); + this.tagResourceAssembler.toResources(findNoteById(id).getTags())); } @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) @ResponseStatus(HttpStatus.NO_CONTENT) void updateNote(@PathVariable("id") long id, @RequestBody NotePatchInput noteInput) { - Note note = this.noteRepository.findById(id).orElseThrow( - () -> new ResourceDoesNotExistException()); + Note note = findNoteById(id); if (noteInput.getTagUris() != null) { note.setTags(getTags(noteInput.getTagUris())); } @@ -123,14 +117,25 @@ void updateNote(@PathVariable("id") long id, @RequestBody NotePatchInput noteInp this.noteRepository.save(note); } + private Note findNoteById(long id) { + Note note = this.noteRepository.findById(id); + if (note == null) { + throw new ResourceDoesNotExistException(); + } + return note; + } + private List getTags(List tagLocations) { - return tagLocations - .stream() - .map(location -> this.tagRepository.findById(extractTagId(location)) - . orElseThrow( - () -> new IllegalArgumentException("The tag '" + location - + "' does not exist"))) - .collect(Collectors.toList()); + List tags = new ArrayList<>(tagLocations.size()); + for (URI tagLocation: tagLocations) { + Tag tag = this.tagRepository.findById(extractTagId(tagLocation)); + if (tag == null) { + throw new IllegalArgumentException("The tag '" + tagLocation + + "' does not exist"); + } + tags.add(tag); + } + return tags; } private long extractTagId(URI tagLocation) { diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java index 97fbffcb3..c92d9e238 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -16,12 +16,10 @@ package com.example.notes; -import java.util.Optional; - import org.springframework.data.repository.CrudRepository; public interface TagRepository extends CrudRepository { - Optional findById(long id); + Tag findById(long id); } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java index f29867c58..bc22f0661 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -79,24 +79,29 @@ void delete(@PathVariable("id") long id) { @RequestMapping(value = "/{id}", method = RequestMethod.GET) Resource tag(@PathVariable("id") long id) { - Tag tag = this.repository.findById(id).orElseThrow( - () -> new ResourceDoesNotExistException()); + Tag tag = findTagById(id); return this.tagResourceAssembler.toResource(tag); } @RequestMapping(value = "/{id}/notes", method = RequestMethod.GET) ResourceSupport tagNotes(@PathVariable("id") long id) { + Tag tag = findTagById(id); return new NestedContentResource( - this.noteResourceAssembler.toResources(this.repository.findById(id) - .orElseThrow(() -> new ResourceDoesNotExistException()) - .getNotes())); + this.noteResourceAssembler.toResources(tag.getNotes())); + } + + private Tag findTagById(long id) { + Tag tag = this.repository.findById(id); + if (tag == null) { + throw new ResourceDoesNotExistException(); + } + return tag; } @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) @ResponseStatus(HttpStatus.NO_CONTENT) void updateTag(@PathVariable("id") long id, @RequestBody TagPatchInput tagInput) { - Tag tag = this.repository.findById(id).orElseThrow( - () -> new ResourceDoesNotExistException()); + Tag tag = findTagById(id); if (tagInput.getName() != null) { tag.setName(tagInput.getName()); } From 68198b3145e07c4e2769369f1e97f2825b3c9ca2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 May 2015 13:16:21 +0100 Subject: [PATCH 0070/1059] Increase MaxPermSize so that the build passes when using Java 7 --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index 99f673503..8c4851838 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ version=0.1.0.BUILD-SNAPSHOT +org.gradle.jvmargs=-XX:MaxPermSize=512M From 0c0d3b89bd89436ab9a9c72a9a2aaf11f5c27b2c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 May 2015 18:11:48 +0100 Subject: [PATCH 0071/1059] Default snippet encoding to UTF-8 and make it configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, snippets were written to disk using the JVM’s default encoding. While this could be controlled by the file.encoding system property it was not ideal as, if the proper was not always set, it could lead to platform-dependent output being generated. Furthermore, Asciidoctor uses UTF-8 encoding by default so there was a risk of the snippets being generated in a different encoding to the encoding expected by Asciidoctor. This commit updates the configuration so that, by default, snippets are encoded as UTF-8. The RestDocumentationConfigurer API has been enhanced to allow this default to be configured as part of building the MockMvc instance. This enhancement as brought with it a more fluent configuration API that separates configuration that is related to URIs from configuration that is related to snippets. Closes gh-67 --- README.md | 40 ++++- .../com/example/notes/ApiDocumentation.java | 15 +- .../notes/GettingStartedDocumentation.java | 4 +- .../com/example/notes/ApiDocumentation.java | 4 +- .../notes/GettingStartedDocumentation.java | 4 +- .../restdocs/RestDocumentation.java | 13 ++ .../restdocs/config/AbstractConfigurer.java | 35 ++++ .../config/AbstractNestedConfigurer.java | 55 +++++++ .../restdocs/config/NestedConfigurer.java | 35 ++++ .../config/RestDocumentationConfigurer.java | 154 +++++++----------- .../config/RestDocumentationContext.java | 15 ++ .../restdocs/config/SnippetConfigurer.java | 61 +++++++ .../restdocs/config/UriConfigurer.java | 122 ++++++++++++++ .../snippet/SnippetWritingResultHandler.java | 9 +- .../RestDocumentationConfigurerTests.java | 54 ++++-- 15 files changed, 499 insertions(+), 121 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java diff --git a/README.md b/README.md index 9f8ca3f5b..c59611158 100644 --- a/README.md +++ b/README.md @@ -243,24 +243,48 @@ documenting. A custom `ResultHandler` is used to produce individual documentatio snippets for its request and its response as well as a snippet that contains both its request and its response. -You can configure the scheme, host, and port of any URIs that appear in the -documentation snippets: +The first step is to create a `MockMvc` instance using `MockMvcBuilders`, configuring +it by applying a `RestDocumentationConfigurer` that can be obtained from the static +`RestDocumentation.documentationConfiguration` method: ```java @Before public void setUp() { this.mockMvc = MockMvcBuilders .webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer() - .withScheme("https") - .withHost("localhost") - .withPort(8443)) + .apply(documentationConfiguration()) .build(); } ``` -The default values are `http`, `localhost`, and `8080`. You can omit the above -configuration if these defaults meet your needs. +This will apply the default REST documentation configuration: + +| Setting | Default value +| ---------------- | ------------- +| Scheme | http +| Host | localhost +| Port | 8080 +| Context path | Empty string +| Snippet encoding | UTF-8 + +One or more of these settings can be overridden: + +```java +@Before +public void setUp() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(documentationConfiguration() + .uris() + .withScheme("https") + .withHost("api.example.com") + .withPort(443) + .withContextPath("/v3") + .and().snippets() + .withEncoding("ISO-8859-1")) + .build(); +} +``` To document a MockMvc call, you use MockMvc's `andDo` method, passing it a `RestDocumentationResultHandler` that can be easily obtained from 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 7e1f79337..80a762ad2 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 @@ -18,6 +18,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -40,7 +41,6 @@ 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; @@ -72,7 +72,18 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(documentationConfiguration()).build(); + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(documentationConfiguration() + .uris() + .withScheme("https") + .withHost("localhost") + .withPort(8443) + .and().snippets() + .withEncoding("ISO-8859-1")) + .build(); + } @Test diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 1657af9a1..aca7a580c 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.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.RestDocumentation.documentationConfiguration; 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; @@ -38,7 +39,6 @@ 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.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -66,7 +66,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()) + .apply(documentationConfiguration()) .alwaysDo(document("{method-name}/{step}/")) .build(); } 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 f8ce6fa18..4f3975695 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 @@ -19,6 +19,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.RestDocumentation.documentationConfiguration; 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; @@ -40,7 +41,6 @@ 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; @@ -72,7 +72,7 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(documentationConfiguration()).build(); } @Test diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 5b95a35a8..2ca0d48bf 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.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.RestDocumentation.documentationConfiguration; 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; @@ -38,7 +39,6 @@ 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.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -66,7 +66,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()) + .apply(documentationConfiguration()) .alwaysDo(document("{method-name}/{step}/")) .build(); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index f0320e91e..d3a5226dc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -16,11 +16,14 @@ package org.springframework.restdocs; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.restdocs.response.ResponsePostProcessor; import org.springframework.restdocs.response.ResponsePostProcessors; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; /** * Static factory methods for documenting RESTful APIs using Spring MVC Test @@ -33,6 +36,16 @@ private RestDocumentation() { } + /** + * Provides access to a {@link MockMvcConfigurer} that can be used to configure the + * REST documentation when building a {@link MockMvc} instance. + * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * @return the configurer + */ + public static RestDocumentationConfigurer documentationConfiguration() { + return new RestDocumentationConfigurer(); + } + /** * Documents the API call to the given {@code outputDir}. * diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java new file mode 100644 index 000000000..a8120c525 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -0,0 +1,35 @@ +/* + * 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.config; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Abstract configurer that declares methods that are internal to the documentation + * configuration implementation. + * + * @author Andy Wilkinson + */ +abstract class AbstractConfigurer { + + /** + * Applies the configuration, possibly be modifying the given {@code request} + * @param request the request that may be modified + */ + abstract void apply(MockHttpServletRequest request); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java new file mode 100644 index 000000000..ce542926e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -0,0 +1,55 @@ +/* + * 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.config; + +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; + +/** + * Base class for {@link NestedConfigurer} implementations. + * + * @author Andy Wilkinson + * @param The type of the configurer's parent + */ +abstract class AbstractNestedConfigurer extends + AbstractConfigurer implements NestedConfigurer, MockMvcConfigurer { + + private final PARENT parent; + + protected AbstractNestedConfigurer(PARENT parent) { + this.parent = parent; + } + + @Override + public PARENT and() { + return this.parent; + } + + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + this.parent.afterConfigurerAdded(builder); + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return this.parent.beforeMockMvcCreated(builder, context); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java new file mode 100644 index 000000000..25c577673 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -0,0 +1,35 @@ +/* + * 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.config; + +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; + +/** + * A configurer that is nested and, therefore, has a parent. + * + * @author awilkinson + * @param The parent's type + */ +public interface NestedConfigurer { + + /** + * Returns the configurer's parent + * + * @return the parent + */ + PARENT and(); +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index bdb56c938..8d5781462 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -16,7 +16,11 @@ package org.springframework.restdocs.config; +import java.util.Arrays; +import java.util.List; + import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -30,120 +34,84 @@ * @author Andy Wilkinson * @author Dmitriy Mayboroda * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * @see RestDocumentation#documentationConfiguration() */ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { - /** - * The default scheme for documented URIs - * @see #withScheme(String) - */ - public static final String DEFAULT_SCHEME = "http"; + private final UriConfigurer uriConfigurer = new UriConfigurer(this); - /** - * The defalt host for documented URIs - * @see #withHost(String) - */ - public static final String DEFAULT_HOST = "localhost"; + private final SnippetConfigurer snippetConfigurer = new SnippetConfigurer(this); - /** - * The default port for documented URIs - * @see #withPort(int) - */ - public static final int DEFAULT_PORT = 8080; + private final RequestPostProcessor requestPostProcessor; /** - * The default context path for documented URIs - * @see #withContextPath(String) + * Creates a new {@link RestDocumentationConfigurer}. + * @see RestDocumentation#documentationConfiguration() */ - public static final String DEFAULT_CONTEXT_PATH = ""; + public RestDocumentationConfigurer() { + this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( + Arrays. asList(this.uriConfigurer, + this.snippetConfigurer, new StepCountConfigurer(), + new ContentLengthHeaderConfigurer())); + } - private String scheme = DEFAULT_SCHEME; + public UriConfigurer uris() { + return this.uriConfigurer; + } - private String host = DEFAULT_HOST; + public SnippetConfigurer snippets() { + return this.snippetConfigurer; + } - private int port = DEFAULT_PORT; + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return this.requestPostProcessor; + } - private String contextPath = DEFAULT_CONTEXT_PATH; + private static class StepCountConfigurer extends AbstractConfigurer { - /** - * Configures any documented URIs to use the given {@code scheme}. The default is - * {@code http}. - * - * @param scheme The URI scheme - * @return {@code this} - */ - public RestDocumentationConfigurer withScheme(String scheme) { - this.scheme = scheme; - return this; - } + @Override + void apply(MockHttpServletRequest request) { + RestDocumentationContext currentContext = RestDocumentationContext + .currentContext(); + if (currentContext != null) { + currentContext.getAndIncrementStepCount(); + } + } - /** - * Configures any documented URIs to use the given {@code host}. The default is - * {@code localhost}. - * - * @param host The URI host - * @return {@code this} - */ - public RestDocumentationConfigurer withHost(String host) { - this.host = host; - return this; } - /** - * Configures any documented URIs to use the given {@code port}. The default is - * {@code 8080}. - * - * @param port The URI port - * @return {@code this} - */ - public RestDocumentationConfigurer withPort(int port) { - this.port = port; - return this; - } + private static class ContentLengthHeaderConfigurer extends AbstractConfigurer { + + @Override + void apply(MockHttpServletRequest request) { + long contentLength = request.getContentLengthLong(); + if (contentLength > 0 + && !StringUtils.hasText(request.getHeader("Content-Length"))) { + request.addHeader("Content-Length", request.getContentLengthLong()); + } + } - /** - * Configures any documented URIs to use the given {@code contextPath}. The default is - * an empty string. - * - * @param contextPath The context path - * @return {@code this} - */ - public RestDocumentationConfigurer withContextPath(String contextPath) { - this.contextPath = (StringUtils.hasText(contextPath) && !contextPath - .startsWith("/")) ? "/" + contextPath : contextPath; - return this; } - @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new RequestPostProcessor() { - - @Override - public MockHttpServletRequest postProcessRequest( - MockHttpServletRequest request) { - RestDocumentationContext currentContext = RestDocumentationContext - .currentContext(); - if (currentContext != null) { - currentContext.getAndIncrementStepCount(); - } - request.setScheme(RestDocumentationConfigurer.this.scheme); - request.setServerPort(RestDocumentationConfigurer.this.port); - request.setServerName(RestDocumentationConfigurer.this.host); - request.setContextPath(RestDocumentationConfigurer.this.contextPath); - configureContentLengthHeaderIfAppropriate(request); - return request; - } + private static class ConfigurerApplyingRequestPostProcessor implements + RequestPostProcessor { + + private final List configurers; + + private ConfigurerApplyingRequestPostProcessor( + List configurers) { + this.configurers = configurers; + } - private void configureContentLengthHeaderIfAppropriate( - MockHttpServletRequest request) { - long contentLength = request.getContentLengthLong(); - if (contentLength > 0 - && !StringUtils.hasText(request.getHeader("Content-Length"))) { - request.addHeader("Content-Length", request.getContentLengthLong()); - } + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + for (AbstractConfigurer configurer : this.configurers) { + configurer.apply(request); } + return request; + } - }; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java index 8d409952e..cbeeca1ed 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java @@ -33,6 +33,8 @@ public class RestDocumentationContext { private final Method testMethod; + private String snippetEncoding; + private RestDocumentationContext() { this(null); } @@ -68,6 +70,19 @@ public int getStepCount() { return this.stepCount.get(); } + void setSnippetEncoding(String snippetEncoding) { + this.snippetEncoding = snippetEncoding; + } + + /** + * Gets the encoding to be used when writing snippets + * + * @return The snippet encoding + */ + public String getSnippetEncoding() { + return this.snippetEncoding; + } + static void establishContext(Method testMethod) { CONTEXTS.set(new RestDocumentationContext(testMethod)); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java new file mode 100644 index 000000000..e94cb3316 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -0,0 +1,61 @@ +/* + * 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.config; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * A configurer that can be used to configure the generated documentation snippets. + * + * @author Andy Wilkinson + * + */ +public class SnippetConfigurer extends + AbstractNestedConfigurer { + + /** + * The default encoding for documentation snippets + * @see #withEncoding(String) + */ + public static final String DEFAULT_SNIPPET_ENCODING = "UTF-8"; + + private String snippetEncoding = DEFAULT_SNIPPET_ENCODING; + + SnippetConfigurer(RestDocumentationConfigurer parent) { + super(parent); + } + + /** + * Configures any documentation snippets to be written using the given + * {@code encoding}. The default is UTF-8. + * @param encoding The encoding + * @return {@code this} + */ + public SnippetConfigurer withEncoding(String encoding) { + this.snippetEncoding = encoding; + return this; + } + + @Override + void apply(MockHttpServletRequest request) { + RestDocumentationContext context = RestDocumentationContext.currentContext(); + if (context != null) { + context.setSnippetEncoding(this.snippetEncoding); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java new file mode 100644 index 000000000..c0fbc03ed --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.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.config; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.StringUtils; + +/** + * A configurer that can be used to configure the documented URIs + * + * @author Andy Wilkinson + */ +public class UriConfigurer extends AbstractNestedConfigurer { + + /** + * The default scheme for documented URIs + * @see #withScheme(String) + */ + public static final String DEFAULT_SCHEME = "http"; + + /** + * The defalt host for documented URIs + * @see #withHost(String) + */ + public static final String DEFAULT_HOST = "localhost"; + + /** + * The default port for documented URIs + * @see #withPort(int) + */ + public static final int DEFAULT_PORT = 8080; + + /** + * The default context path for documented URIs + * @see #withContextPath(String) + */ + public static final String DEFAULT_CONTEXT_PATH = ""; + + private String scheme = DEFAULT_SCHEME; + + private String host = DEFAULT_HOST; + + private int port = DEFAULT_PORT; + + private String contextPath = DEFAULT_CONTEXT_PATH; + + protected UriConfigurer(RestDocumentationConfigurer parent) { + super(parent); + } + + /** + * Configures any documented URIs to use the given {@code scheme}. The default is + * {@code http}. + * + * @param scheme The URI scheme + * @return {@code this} + */ + public UriConfigurer withScheme(String scheme) { + this.scheme = scheme; + return this; + } + + /** + * Configures any documented URIs to use the given {@code host}. The default is + * {@code localhost}. + * + * @param host The URI host + * @return {@code this} + */ + public UriConfigurer withHost(String host) { + this.host = host; + return this; + } + + /** + * Configures any documented URIs to use the given {@code port}. The default is + * {@code 8080}. + * + * @param port The URI port + * @return {@code this} + */ + public UriConfigurer withPort(int port) { + this.port = port; + return this; + } + + /** + * Configures any documented URIs to use the given {@code contextPath}. The default is + * an empty string. + * + * @param contextPath The context path + * @return {@code this} + */ + public UriConfigurer withContextPath(String contextPath) { + this.contextPath = (StringUtils.hasText(contextPath) && !contextPath + .startsWith("/")) ? "/" + contextPath : contextPath; + return this; + } + + @Override + void apply(MockHttpServletRequest request) { + request.setScheme(this.scheme); + request.setServerPort(this.port); + request.setServerName(this.host); + request.setContextPath(this.contextPath); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 819ec6c42..d77635e6a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -17,11 +17,13 @@ package org.springframework.restdocs.snippet; import java.io.File; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -65,7 +67,12 @@ private Writer createWriter() throws IOException { throw new IllegalStateException("Failed to create directory '" + parent + "'"); } - return new FileWriter(outputFile); + RestDocumentationContext context = RestDocumentationContext.currentContext(); + if (context == null || context.getSnippetEncoding() == null) { + return new FileWriter(outputFile); + } + return new OutputStreamWriter(new FileOutputStream(outputFile), + context.getSnippetEncoding()); } else { return new OutputStreamWriter(System.out); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 1ec54d306..35e95c1d6 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -52,7 +52,7 @@ public void defaultConfiguration() { @Test public void customScheme() { - RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() .withScheme("https").beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -61,8 +61,8 @@ public void customScheme() { @Test public void customHost() { - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withHost( - "api.example.com").beforeMockMvcCreated(null, null); + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() + .withHost("api.example.com").beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "api.example.com", 8080); @@ -70,8 +70,8 @@ public void customHost() { @Test public void customPort() { - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( - 8081).beforeMockMvcCreated(null, null); + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() + .withPort(8081).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "localhost", 8081); @@ -80,7 +80,7 @@ public void customPort() { @Test public void customContextPathWithoutSlash() { String contextPath = "context-path"; - RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() .withContextPath(contextPath).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -91,7 +91,7 @@ public void customContextPathWithoutSlash() { @Test public void customContextPathWithSlash() { String contextPath = "/context-path"; - RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() .withContextPath(contextPath).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -101,16 +101,16 @@ public void customContextPathWithSlash() { @Test public void noContentLengthHeaderWhenRequestHasNotContent() { - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( - 8081).beforeMockMvcCreated(null, null); + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() + .withPort(8081).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertThat(this.request.getHeader("Content-Length"), is(nullValue())); } @Test public void contentLengthHeaderIsSetWhenRequestHasContent() { - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( - 8081).beforeMockMvcCreated(null, null); + RequestPostProcessor postProcessor = new RestDocumentationConfigurer() + .beforeMockMvcCreated(null, null); byte[] content = "Hello, world".getBytes(); this.request.setContent(content); postProcessor.postProcessRequest(this.request); @@ -118,6 +118,38 @@ public void contentLengthHeaderIsSetWhenRequestHasContent() { is(equalTo(Integer.toString(content.length)))); } + @Test + public void defaultSnippetEncodingIsAppliedToTheContext() { + RestDocumentationContext.establishContext(null); + try { + assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), + is(nullValue())); + new RestDocumentationConfigurer().beforeMockMvcCreated(null, null) + .postProcessRequest(this.request); + assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), + is(equalTo("UTF-8"))); + } + finally { + RestDocumentationContext.clearContext(); + } + } + + @Test + public void customSnippetEncodingIsAppliedToTheContext() { + RestDocumentationContext.establishContext(null); + try { + assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), + is(nullValue())); + new RestDocumentationConfigurer().snippets().withEncoding("foo") + .beforeMockMvcCreated(null, null).postProcessRequest(this.request); + assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), + is(equalTo("foo"))); + } + finally { + RestDocumentationContext.clearContext(); + } + } + private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); From f98bec317ec5efbfc56630ee02ac50b7df167d7f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 May 2015 21:05:13 +0100 Subject: [PATCH 0072/1059] Add support for documenting fields in array payloads 764daf7 added support for documenting fields in payloads that contain arrays, but the array had to be nested within a map. This commit builds on that support to allow fields in an array payload to be documented. The fields in the payload: [ { "a": { "b": 5 } }, { "a": { "c": "charlie" } } ] Can be documented using: []a.b []a.c []a A dot separator can, optionally, be used between the [] and the map key: [].a.b [].a.c [].a Closes gh-69 --- .../restdocs/payload/FieldPath.java | 30 +++++--- .../restdocs/payload/FieldProcessor.java | 69 ++++++------------- .../payload/FieldSnippetResultHandler.java | 13 +--- .../restdocs/payload/FieldTypeResolver.java | 2 +- .../restdocs/payload/FieldValidator.java | 22 +++--- .../restdocs/payload/FieldPathTests.java | 52 ++++++++++++++ .../restdocs/payload/FieldValidatorTests.java | 23 +++++++ .../payload/PayloadDocumentationTests.java | 45 ++++++++++-- 8 files changed, 170 insertions(+), 86 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java index e72c6283b..cbb87a12a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java @@ -75,24 +75,32 @@ static boolean matchesSingleValue(List segments) { return true; } - static List extractSegments(String path) { + private static List extractSegments(String path) { Matcher matcher = ARRAY_INDEX_PATTERN.matcher(path); - String processedPath; - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); + int previous = 0; while (matcher.find()) { - matcher.appendReplacement(buffer, ".[$1]"); + appendWithSeparatorIfNecessary(buffer, + path.substring(previous, matcher.start(0))); + appendWithSeparatorIfNecessary(buffer, matcher.group()); + previous = matcher.end(0); } - matcher.appendTail(buffer); - - if (buffer.length() > 0) { - processedPath = buffer.toString(); - } - else { - processedPath = path; + if (previous < path.length()) { + appendWithSeparatorIfNecessary(buffer, path.substring(previous)); } + String processedPath = buffer.toString(); + return Arrays.asList(processedPath.indexOf('.') > -1 ? processedPath.split("\\.") : new String[] { processedPath }); } + private static void appendWithSeparatorIfNecessary(StringBuilder buffer, + String toAppend) { + if (buffer.length() > 0 && (buffer.lastIndexOf(".") != buffer.length() - 1) + && !toAppend.startsWith(".")) { + buffer.append("."); + } + buffer.append(toAppend); + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java index eb9e0ca86..0407e567e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -31,38 +31,26 @@ */ class FieldProcessor { - boolean hasField(FieldPath fieldPath, Map payload) { + boolean hasField(FieldPath fieldPath, Object payload) { final AtomicReference hasField = new AtomicReference(false); traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { @Override - public boolean foundMatch(Match match) { + public void foundMatch(Match match) { hasField.set(true); - return false; - } - - @Override - public boolean matchNotFound() { - return false; } }); return hasField.get(); } - Object extract(final FieldPath path, Map payload) { + Object extract(FieldPath path, Object payload) { final List matches = new ArrayList(); traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override - public boolean foundMatch(Match match) { + public void foundMatch(Match match) { matches.add(match.getValue()); - return true; - } - - @Override - public boolean matchNotFound() { - return false; } }); @@ -78,75 +66,59 @@ public boolean matchNotFound() { } } - void remove(final FieldPath path, final Map payload) { + void remove(final FieldPath path, Object payload) { traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override - public boolean foundMatch(Match match) { + public void foundMatch(Match match) { match.remove(); - return true; - } - - @Override - public boolean matchNotFound() { - return true; } }); } - private boolean traverse(ProcessingContext context, MatchCallback matchCallback) { + private void traverse(ProcessingContext context, MatchCallback matchCallback) { final String segment = context.getSegment(); if (FieldPath.isArraySegment(segment)) { if (context.getPayload() instanceof List) { - return handleListPayload(context, matchCallback); + handleListPayload(context, matchCallback); } } else if (context.getPayload() instanceof Map && ((Map) context.getPayload()).containsKey(segment)) { - return handleMapPayload(context, matchCallback); + handleMapPayload(context, matchCallback); } - - return matchCallback.matchNotFound(); } - private boolean handleListPayload(ProcessingContext context, - MatchCallback matchCallback) { + private void handleListPayload(ProcessingContext context, MatchCallback matchCallback) { List list = context.getPayload(); final Iterator items = list.iterator(); if (context.isLeaf()) { while (items.hasNext()) { Object item = items.next(); - if (!matchCallback.foundMatch(new ListMatch(items, list, item, context - .getParentMatch()))) { - return false; - } + matchCallback.foundMatch(new ListMatch(items, list, item, context + .getParentMatch())); } - return true; } else { - boolean result = true; - while (items.hasNext() && result) { + while (items.hasNext()) { Object item = items.next(); - result = result - && traverse(context.descend(item, new ListMatch(items, list, - item, context.parent)), matchCallback); + traverse(context.descend(item, new ListMatch(items, list, item, + context.parent)), matchCallback); } - return result; } } - private boolean handleMapPayload(ProcessingContext context, - MatchCallback matchCallback) { + private void handleMapPayload(ProcessingContext context, MatchCallback matchCallback) { Map map = context.getPayload(); - final Object item = map.get(context.getSegment()); + Object item = map.get(context.getSegment()); MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), context.getParentMatch()); if (context.isLeaf()) { - return matchCallback.foundMatch(mapMatch); + matchCallback.foundMatch(mapMatch); } else { - return traverse(context.descend(item, mapMatch), matchCallback); + traverse(context.descend(item, mapMatch), matchCallback); } } @@ -216,9 +188,8 @@ public void remove() { private interface MatchCallback { - boolean foundMatch(Match match); + void foundMatch(Match match); - boolean matchNotFound(); } private interface Match { 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 index 7d9bd5337..5ad941578 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -68,7 +68,7 @@ protected void handle(MvcResult result, DocumentationWriter writer) this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - final Map payload = extractPayload(result); + final Object payload = extractPayload(result); writer.table(new TableAction() { @@ -91,15 +91,8 @@ public void perform(TableWriter tableWriter) throws IOException { } - @SuppressWarnings("unchecked") - private Map extractPayload(MvcResult result) throws IOException { - Reader payloadReader = getPayloadReader(result); - try { - return this.objectMapper.readValue(payloadReader, Map.class); - } - finally { - payloadReader.close(); - } + private Object extractPayload(MvcResult result) throws IOException { + return this.objectMapper.readValue(getPayloadReader(result), Object.class); } protected abstract Reader getPayloadReader(MvcResult result) throws IOException; 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 index 89d2541b3..dff50cdae 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -28,7 +28,7 @@ class FieldTypeResolver { private final FieldProcessor fieldProcessor = new FieldProcessor(); - FieldType resolveFieldType(String path, Map payload) { + FieldType resolveFieldType(String path, Object payload) { FieldPath fieldPath = FieldPath.compile(path); Object field = this.fieldProcessor.extract(fieldPath, payload); if (field instanceof Collection && !fieldPath.isPrecise()) { 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 index 4404ff685..9055cc878 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -40,18 +40,15 @@ class FieldValidator { 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); + Object payload = this.objectMapper.readValue(payloadReader, Object.class); List missingFields = findMissingFields(payload, fieldDescriptors); - Map undocumentedPayload = findUndocumentedFields(payload, - fieldDescriptors); + Object undocumentedPayload = findUndocumentedFields(payload, fieldDescriptors); - if (!missingFields.isEmpty() || !undocumentedPayload.isEmpty()) { + if (!missingFields.isEmpty() || !isEmpty(undocumentedPayload)) { String message = ""; - if (!undocumentedPayload.isEmpty()) { + if (!isEmpty(undocumentedPayload)) { message += String.format( "The following parts of the payload were not documented:%n%s", this.objectMapper.writeValueAsString(undocumentedPayload)); @@ -67,7 +64,14 @@ void validate(Reader payloadReader, List fieldDescriptors) } } - private List findMissingFields(Map payload, + private boolean isEmpty(Object object) { + if (object instanceof Map) { + return ((Map) object).isEmpty(); + } + return ((List) object).isEmpty(); + } + + private List findMissingFields(Object payload, List fieldDescriptors) { List missingFields = new ArrayList(); @@ -82,7 +86,7 @@ private List findMissingFields(Map payload, return missingFields; } - private Map findUndocumentedFields(Map payload, + private Object findUndocumentedFields(Object payload, List fieldDescriptors) { for (FieldDescriptor fieldDescriptor : fieldDescriptors) { FieldPath path = FieldPath.compile(fieldDescriptor.getPath()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java index e082aaf1e..0e1857201 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java @@ -16,7 +16,9 @@ package org.springframework.restdocs.payload; +import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -38,6 +40,16 @@ public void singleNestedFieldIsPrecise() { assertTrue(FieldPath.compile("a.b").isPrecise()); } + @Test + public void topLevelArrayIsNotPrecise() { + assertFalse(FieldPath.compile("[]").isPrecise()); + } + + @Test + public void fieldBeneathTopLevelArrayIsNotPrecise() { + assertFalse(FieldPath.compile("[]a").isPrecise()); + } + @Test public void arrayIsNotPrecise() { assertFalse(FieldPath.compile("a[]").isPrecise()); @@ -58,4 +70,44 @@ public void fieldBeneathAnArrayIsNotPrecise() { assertFalse(FieldPath.compile("a[].b").isPrecise()); } + @Test + public void compilationOfSingleElementPath() { + assertThat(FieldPath.compile("a").getSegments(), contains("a")); + } + + @Test + public void compilationOfMultipleElementPath() { + assertThat(FieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c")); + } + + @Test + public void compilationOfPathWithArraysWithNoDotSeparators() { + assertThat(FieldPath.compile("a[]b[]c").getSegments(), + contains("a", "[]", "b", "[]", "c")); + } + + @Test + public void compilationOfPathWithArraysWithPreAndPostDotSeparators() { + assertThat(FieldPath.compile("a.[].b.[].c").getSegments(), + contains("a", "[]", "b", "[]", "c")); + } + + @Test + public void compilationOfPathWithArraysWithPreDotSeparators() { + assertThat(FieldPath.compile("a.[]b.[]c").getSegments(), + contains("a", "[]", "b", "[]", "c")); + } + + @Test + public void compilationOfPathWithArraysWithPostDotSeparators() { + assertThat(FieldPath.compile("a[].b[].c").getSegments(), + contains("a", "[]", "b", "[]", "c")); + } + + @Test + public void compilationOfPathStartingWithAnArray() { + assertThat(FieldPath.compile("[]a.b.c").getSegments(), + contains("[]", "a", "b", "c")); + } + } 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 index 081e3be5f..e8ec0f299 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -38,6 +38,9 @@ public class FieldValidatorTests { @Rule public ExpectedException thrownException = ExpectedException.none(); + private StringReader listPayload = new StringReader( + "[{\"a\":1},{\"a\":2},{\"b\":{\"c\":3}}]"); + private StringReader payload = new StringReader( "{\"a\":{\"b\":{},\"c\":true,\"d\":[{\"e\":1},{\"e\":2}]}}"); @@ -88,4 +91,24 @@ public void undocumentedField() throws IOException { this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.d"))); } + + @Test + public void listPayloadNoMissingFieldsAllFieldsDocumented() throws IOException { + this.fieldValidator.validate(this.listPayload, Arrays.asList(new FieldDescriptor( + "[]b.c"), new FieldDescriptor("[]b"), new FieldDescriptor("[]a"), + new FieldDescriptor("[]"))); + } + + @Test + public void listPayloadParentIsDocumentedWhenAllChildrenAreDocumented() + throws IOException { + this.fieldValidator.validate(this.listPayload, + Arrays.asList(new FieldDescriptor("[]b.c"), new FieldDescriptor("[]a"))); + } + + @Test + public void listPayloadChildIsDocumentedWhenParentIsDocumented() throws IOException { + this.fieldValidator.validate(this.listPayload, + Arrays.asList(new FieldDescriptor("[]"))); + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 38a8d38c0..e8750ccfa 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -48,14 +48,14 @@ public class PayloadDocumentationTests { public final ExpectedSnippet snippet = new ExpectedSnippet(); @Test - public void requestWithFields() throws IOException { - this.snippet.expectRequestFields("request-with-fields").withContents( // + public void mapRequestWithFields() throws IOException { + this.snippet.expectRequestFields("map-request-with-fields").withContents( // tableWithHeader("Path", "Type", "Description") // .row("a.b", "Number", "one") // .row("a.c", "String", "two") // .row("a", "Object", "three")); - documentRequestFields("request-with-fields", + documentRequestFields("map-request-with-fields", fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three")).handle( @@ -63,8 +63,24 @@ public void requestWithFields() throws IOException { } @Test - public void responseWithFields() throws IOException { - this.snippet.expectResponseFields("response-with-fields").withContents(// + public void arrayRequestWithFields() throws IOException { + this.snippet.expectRequestFields("array-request-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]a.b", "Number", "one") // + .row("[]a.c", "String", "two") // + .row("[]a", "Object", "three")); + + documentRequestFields("array-request-with-fields", + fieldWithPath("[]a.b").description("one"), + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three")).handle( + result(get("/foo").content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"))); + } + + @Test + public void mapResponseWithFields() throws IOException { + this.snippet.expectResponseFields("map-response-with-fields").withContents(// tableWithHeader("Path", "Type", "Description") // .row("id", "Number", "one") // .row("date", "String", "two") // @@ -77,7 +93,7 @@ public void responseWithFields() throws IOException { response.getWriter().append( "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}"); - documentResponseFields("response-with-fields", + documentResponseFields("map-response-with-fields", fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), @@ -87,6 +103,23 @@ public void responseWithFields() throws IOException { result(response)); } + @Test + public void arrayResponseWithFields() throws IOException { + this.snippet.expectResponseFields("array-response-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]a.b", "Number", "one") // + .row("[]a.c", "String", "two") // + .row("[]a", "Object", "three")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter() + .append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"); + documentResponseFields("array-response-with-fields", + fieldWithPath("[]a.b").description("one"), + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three")).handle(result(response)); + } + @Test public void undocumentedRequestField() throws IOException { this.thrown.expect(SnippetGenerationException.class); From 8307fb54281ea375316787c0f7f85a26de7429a9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 21 May 2015 16:44:44 +0100 Subject: [PATCH 0073/1059] Add reference documentation and create docs zip Add reference documentation for the project and add a docs artifact that contains both the reference documentation and the javadoc. See gh-37 --- build.gradle | 121 ++++---- docs/build.gradle | 19 ++ docs/src/docs/asciidoc/configuration.adoc | 67 +++++ .../docs/asciidoc/customizing-responses.adoc | 45 +++ .../docs/asciidoc/documenting-your-api.adoc | 263 ++++++++++++++++++ docs/src/docs/asciidoc/getting-started.adoc | 219 +++++++++++++++ docs/src/docs/asciidoc/index.adoc | 23 ++ docs/src/docs/asciidoc/introduction.adoc | 23 ++ docs/src/test/java/com/example/AlwaysDo.java | 42 +++ .../test/java/com/example/CustomEncoding.java | 44 +++ .../com/example/CustomUriConfiguration.java | 47 ++++ .../com/example/ExampleApplicationTests.java | 42 +++ .../src/test/java/com/example/Hypermedia.java | 53 ++++ .../test/java/com/example/InvokeService.java | 38 +++ docs/src/test/java/com/example/Payload.java | 54 ++++ .../java/com/example/QueryParameters.java | 41 +++ .../com/example/ResponsePostProcessing.java | 38 +++ .../rest-notes-spring-data-rest/build.gradle | 10 +- samples/rest-notes-spring-data-rest/pom.xml | 5 +- .../src/main/asciidoc/api-guide.adoc | 62 ++--- .../main/asciidoc/getting-started-guide.adoc | 46 +-- .../rest-notes-spring-hateoas/build.gradle | 10 +- samples/rest-notes-spring-hateoas/pom.xml | 5 +- .../src/main/asciidoc/api-guide.adoc | 62 ++--- .../main/asciidoc/getting-started-guide.adoc | 46 +-- settings.gradle | 1 + spring-restdocs/build.gradle | 89 ++++++ 27 files changed, 1319 insertions(+), 196 deletions(-) create mode 100644 docs/build.gradle create mode 100644 docs/src/docs/asciidoc/configuration.adoc create mode 100644 docs/src/docs/asciidoc/customizing-responses.adoc create mode 100644 docs/src/docs/asciidoc/documenting-your-api.adoc create mode 100644 docs/src/docs/asciidoc/getting-started.adoc create mode 100644 docs/src/docs/asciidoc/index.adoc create mode 100644 docs/src/docs/asciidoc/introduction.adoc create mode 100644 docs/src/test/java/com/example/AlwaysDo.java create mode 100644 docs/src/test/java/com/example/CustomEncoding.java create mode 100644 docs/src/test/java/com/example/CustomUriConfiguration.java create mode 100644 docs/src/test/java/com/example/ExampleApplicationTests.java create mode 100644 docs/src/test/java/com/example/Hypermedia.java create mode 100644 docs/src/test/java/com/example/InvokeService.java create mode 100644 docs/src/test/java/com/example/Payload.java create mode 100644 docs/src/test/java/com/example/QueryParameters.java create mode 100644 docs/src/test/java/com/example/ResponsePostProcessing.java create mode 100644 spring-restdocs/build.gradle diff --git a/build.gradle b/build.gradle index ef09e161f..4f1507248 100644 --- a/build.gradle +++ b/build.gradle @@ -1,87 +1,40 @@ -project(':spring-restdocs') { - - ext { - hamcrestVersion = '1.3' - jacksonVersion = '2.3.4' - jacocoVersion = '0.7.2.201409121644' - junitVersion = '4.11' - servletApiVersion = '3.1.0' - springHateoasVersion = '0.17.0.RELEASE' - springVersion = '4.1.4.RELEASE' - mockitoVersion = '1.10.19' - } - - group = 'org.springframework.restdocs' - - apply plugin: 'java' - apply plugin: 'eclipse' - apply plugin: 'maven' - apply plugin: 'sonar-runner' - - sonarRunner { - sonarProperties { - property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" - property 'sonar.java.coveragePlugin', 'jacoco' - property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' - property 'sonar.links.homepage', 'https://github.com/spring-projects/spring-restdocs' - property 'sonar.links.issue', 'https://github.com/spring-projects/spring-restdocs' - property 'sonar.links.scm', 'https://github.com/spring-projects/spring-restdocs' - } - } - - configurations { - jacoco - } - - sourceCompatibility = 1.7 - targetCompatibility = 1.7 - +buildscript { repositories { jcenter() } - - task sourcesJar(type: Jar) { - classifier = 'sources' - from project.sourceSets.main.allSource - } - - task javadocJar(type: Jar) { - classifier = "javadoc" - from javadoc + dependencies { + classpath 'io.spring.gradle:dependency-management-plugin:0.5.1.RELEASE' } +} - artifacts { - archives sourcesJar - archives javadocJar - } +apply plugin: 'samples' - eclipseJdt.onlyIf { false } - cleanEclipseJdt.onlyIf { false } +ext { + springVersion = '4.1.5.RELEASE' +} - dependencies { - compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" - compile "junit:junit:$junitVersion" - compile "org.springframework:spring-test:$springVersion" - compile "org.springframework:spring-web:$springVersion" - compile "org.springframework:spring-webmvc:$springVersion" - compile "javax.servlet:javax.servlet-api:$servletApiVersion" - jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" - testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion" - testCompile "org.mockito:mockito-core:$mockitoVersion" - testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" - testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" - } +subprojects { + apply plugin: 'io.spring.dependency-management' + apply plugin: 'java' + apply plugin: 'eclipse' - test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" - testLogging { - exceptionFormat "full" + dependencyManagement { + imports { + mavenBom "org.springframework:spring-framework-bom:$springVersion" + } + dependencies { + dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' + dependency 'javax.servlet:javax.servlet-api:3.1.0' + dependency 'junit:junit:4.12' + dependency 'org.hamcrest:hamcrest-core:1.3' + dependency 'org.hamcrest:hamcrest-library:1.3' + dependency 'org.mockito:mockito-core:1.10.19' + dependency 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE' + dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644' } } } -apply plugin: 'samples' - samples { dependOn 'spring-restdocs:install' @@ -94,6 +47,26 @@ samples { } } -wrapper { - gradleVersion = '2.3' +task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':spring-restdocs:javadoc']) { + group = 'Distribution' + baseName = 'spring-restdocs' + classifier = 'docs' + description = 'Builds -${classifier} archive containing API and reference documentation' + destinationDir = file("${project.buildDir}/distributions") + + from (project.tasks.findByPath(':docs:asciidoctor')) { + into 'reference' + } + + from (project.tasks.findByPath(':spring-restdocs:javadoc')) { + into 'api' + } +} + +configurations { + archives +} + +artifacts { + archives docsZip } \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 000000000..f075eb283 --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'org.asciidoctor.convert' version '1.5.2' +} + +dependencies { + compile 'org.springframework:spring-webmvc' + + testCompile project(':spring-restdocs') + testCompile 'junit:junit' + testCompile 'org.springframework:spring-test' +} + +tasks.findByPath("artifactoryPublish")?.enabled = false + +asciidoctor { + sources { + include 'index.adoc' + } +} \ No newline at end of file diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc new file mode 100644 index 000000000..fb92299b3 --- /dev/null +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -0,0 +1,67 @@ +[[configuration]] +== Configuration + + + +[[configuration-uris]] +=== Documented URIs + +The default configuration for URIs documented by Spring REST docs is: + +|=== +|Setting |Default + +|Scheme +|`http` + +|Host +|`localhost` + +|Port +|`8080` + +|Context path +|Empty string +|=== + +This configuration is applied by `RestDocumentationConfigurer`. You can use its API to +change one or more of the defaults to suit your needs: + +[source,java,indent=0] +---- +include::{examples-dir}com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] +---- + + + +[[configuration-snippet-encoding]] +=== Snippet encoding + +The default encoding used by Asciidoc is `UTF-8`. Spring REST docs adopts the same +default for the snippets that it generated. If you require an encoding other than `UTF-8`, +use `RestDocumentationConfigurer` to configure it: + +[source,java,indent=0] +---- +include::{examples-dir}com/example/CustomEncoding.java[tags=custom-encoding] +---- + + + +[[configuration-output-directory]] +=== Snippet output directory + +As described in <> the snippet output directory is +configured in your `pom.xml` or `build.gradle` file. This configuration applies to builds +on the command line, but it may not apply when running your tests in your IDE. In the +absence of the property, Spring REST Docs will write the generated snippets to standard +out. + +If you'd prefer that your IDE writes the snippets to disk you can use a file in +`src/test/resources` named `documentation.properties` to specify the output directory that +should be used: + +[source,properties] +---- +org.springframework.restdocs.outputDir: target/generated-snippets +---- \ No newline at end of file diff --git a/docs/src/docs/asciidoc/customizing-responses.adoc b/docs/src/docs/asciidoc/customizing-responses.adoc new file mode 100644 index 000000000..08613f938 --- /dev/null +++ b/docs/src/docs/asciidoc/customizing-responses.adoc @@ -0,0 +1,45 @@ +[[customizing-responses]] +== Customizing responses + +There may be situations where you do not want to document a response exactly as received. +Spring REST Docs provides a number of response post processors that can be used to modify +a response after it is received but before it's documented. + +Response modification is configured using a `ResponseModifier`. An instance can be +obtained using the static `modifyResponseTo` method on `RestDocumentation`. Once the +response modifications have been provided, documentation can be configured as usual +via the `andDocument` method: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/ResponsePostProcessing.java[tags=general] +---- +<1> Call `modifyResponseTo` to configure response modifications, passing in one or more +`ResponsePostProcessor` implementations. +<2> Proceed with documenting the call + + +[[customizing-responses-pretty-printing]] +=== Pretty printing + +`prettyPrintContent` on `ResponsePostProcessors` formats the body of the response to +make it easier to read. + +[[customizing-responses-masking-links]] +=== Masking links + +If you're documenting a Hypermedia-based API, you may want to encourage clients to +navigate the API using links rather than through the use of hard coded URIs. One way to do +this is to limit the use of URIs in the documentation. `maskLinks` on +`ResponsePostProcessors` replaces the `href` of any links in the response with `...`. A +different replacement can also be specified if you wish. + +=== Removing headers + +`removeHeaders` on `ResponsePostProcessors` removes any occurrences of the named headers +from the response. + +=== Replacing patterns + +`replacePattern` on `ResponsePostProcessors` provides a general purpose mechanism for +replacing content in a response. Any occurrences of a regular expression are replaced. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc new file mode 100644 index 000000000..dad31532b --- /dev/null +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -0,0 +1,263 @@ +[[documenting-your-api]] +== Documenting your API + +This section provides more details about using Spring REST Docs to document your API. + + + +[[documenting-your-api-hypermedia]] +=== Hypermedia + +Spring REST Docs provides support for documenting the links in a +https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Hypermedia.java[tag=links] +---- +<1> `withLinks` is used to describe the expected links +<2> Expects a link whose rel is `alpha`. Uses the static `linkWithRel` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<3> Expects a link whose rel is `bravo` + +The result is a snippet named `links.adoc` that contains a table describing the resource's +links. + +When documenting links, the test will fail if an undocumented link is found in the +response. Similarly, the test will also fail if a documented link is not found in the +response. + + + +[[documenting-your-api-hypermedia-link-formats]] +==== Hypermedia link formats + +Two link formats are understood by default: + + * Atom – links are expected to be in an array named `links`. Used by default when the + content type of the response is compatible with `application/json`. + * HAL – links are expected to be in a map named `_links`. Used by default when the + content type of the response is compatible with `application/hal+json`. + +If you are using Atom or HAL-format links but with a different content type you can +provide one of the built-in `LinkExtractor` implementations to `withLinks`. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] +---- +<1> Indicate that the links are in HAL format using the `halLinks` static method on +`org.springframework.restdocs.hypermedia.LinkExtractors`. + +If your API represents its links in a format other than Atom or HAL you can provide your +own implementation of the `LinkExtractor` interface to extract the links from the +response. + +[[documenting-your-api-request-response-payloads]] +=== Request and response payloads + +In addition to the Hypermedia-specific support <>, support for general documentation of request and response payloads is also +provided: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=response] +---- +<1> `withResponseFields` is used to describe the expected fields in the response payload. +To document a request `withRequestFields` can be used. +<2> Expects a field with the path `contact`. Uses the static `fieldWithPath` method on +`org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expects a field with the path `contact.email` + +The result is a snippet that contains a table describing the fields. For requests this +snippet is named `request-fields.adoc`. For responses this snippet is named +`response-fields.adoc`. + +When documenting fields, the test will fail if an undocumented field is found in the +payload. Similarly, the test will also fail if a documented field is not found in the +payload and the field has not be marked as optional. For payloads with a hierarchical +structure, when a point in the hierarchy is documented any fields beneath that point are +also deemed to have been documented. This means that you do not have to document the +entire hierarchy, but you may do so if you wish. + + + +[[documenting-your-api-request-response-payloads-field-paths]] +==== Field paths + +When documenting request and response payloads, fields are identified using a path. Paths +use `.` to descend into a child object and `[]` to identify an array. For example, with +this JSON payload: + +[source,json,indent=0] +---- + { + "a":{ + "b":[ + { + "c":"one" + }, + { + "c":"two" + }, + { + "d":"three" + } + ] + } + } +---- + +The following paths are all present: + +[cols="1,3"] +|=== +|Path | Value + +|`a` +|An object containing `b` + +|`a.b` +|An array containing three objects + +|`a.b[]` +|An array containing three objects + +|`a.b[].c` +|An array containing the strings `one` and `two` + +|`a.b[].d` +|The string `three` +|=== + + + +[[documenting-your-api-request-response-payloads-field-types]] +==== Field types + +When a field is documented, Spring REST docs will attempt to determine its type by +examining the payload. Seven different types are supported: + +[cols="1,3"] +|=== +| Type | Description + +| array +| The value of each occurrence of the field is an array + +| boolean +| The value of each occurrence of the field is a boolean (`true` or `false`) + +| object +| The value of each occurrence of the field is an object + +| number +| The value of each occurrence of the field is a number + +| null +| The value of each occurrence of the field is `null` + +| string +| The value of each occurrence of the field is a string + +| varies +| The field occurs multiple times in the payload with a variety of different types +|=== + +The type can also be set explicitly using the `type(FieldType)` method on +`FieldDescriptor`: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=explicit-type] +---- +<1> Set the field's type to `string`. + + + +[[documenting-your-api-query-parameters]] +=== Query parameters + +A request's query parameters can be documented using `withQueryParameters` + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters] +---- +<1> `withQueryParameters` is used to describe the query parameters +<2> Documents a parameter named `page`. Uses the static `parameterWithName` method on +`org.springframework.restdocs.request.RequestDocumentation`. +<3> Documents a parameter named `per_page` + +The result is a snippet named `query-parameters.adoc` that contains a table describing +the query parameters that are supported by the resource. + +When documenting query parameters, the test will fail if an undocumented query parameter +is used in the request. Similarly, the test will also fail if a documented query parameter +is not found in the request. + + + +[[documenting-your-api-default-snippets]] +=== Default snippets + +A number of snippets are produced automatically when you document a call to +`MockMvc.perform`: + +[cols="1,3"] +|=== +|Snippet | Description + +| `curl-request.adoc` +| Contains the http://curl.haxx.se[`curl`] command that is equivalent to the `MockMvc` +call that is being documented + +| `http-request.adoc` +| Contains the HTTP request that is equivalent to the `MockMvc` call that is being +documented + +| `http-response.adoc` +| Contains the HTTP response that was returned +|=== + + + +[[documentating-your-api-parameterized-output-directories]] +=== Using parameterized output directories + +The output directory used by `document` can be parameterized. The following parameters +are supported: + +[cols="1,3"] +|=== +| Parameter | Description + +| {methodName} +| The name of the test method, formatted using camelCase + +| {method-name} +| The name of the test method, formatted using kebab-case + +| {method_name} +| The name of the test method, formatted using snake_case + +| {step} +| The count of calls to MockMvc.perform in the current test +|=== + +For example, `document("{method-name}")` in a test method named `creatingANote` will write +snippets into a directory named `creating-a-note`. + +The `{step}` parameter is particularly useful in combination with Spring MVC Test's +`alwaysDo` functionality. It allows documentation to be configured once in a setup method: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/AlwaysDo.java[tags=always-do] +---- + +With this configuration in place, every call to `MockMvc.perform` will produce +the <> without any further +configuration. Take a look at the `GettingStartedDocumentation` classes in each of the +sample applications to see this functionality in action. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc new file mode 100644 index 000000000..614036d68 --- /dev/null +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -0,0 +1,219 @@ +[[getting-started]] +== Getting started + +This section describes how to get started with Spring REST docs. + + + +[[getting-started-sample-applications]] +=== Sample applications + +If you want to jump straight in, there are two sample applications available. +{hateoas-sample}[One sample uses Spring HATEOAS] and {data-rest-sample}[the other uses +Spring Data REST]. Both samples use Spring REST Docs to produce a detailed API guide +and a getting started walkthrough. You can use either Gradle or Maven to build them. + +In each sample the source for the documentation can be found in `src/main/asciidoc`. +`api-guide.adoc` produces an API guide for the service. `getting-started-guide.adoc` +produces a getting started guide that provides an introductory walkthrough. + +The code that produces the generated snippets can be found in `src/test/java`. +`ApiDocumentation.java` produces the snippets for the API guide. +`GettingStartedDocumentation.java` produces the snippets for the getting started guide. + + + +[[getting-started-build-configuration]] +=== Build configuration + +The first step in using Spring REST Docs is to configure your project's build. + + + +[[getting-started-build-configuration-gradle]] +==== Gradle build configuration + +Both {samples}[sample applications] contain `build.gradle` files that you may wish to +use as a reference. The key parts of the configuration are described below. + + + +[source,groovy,indent=0] +---- + plugins { <1> + id "org.asciidoctor.convert" version "1.5.2" + } + + dependencies { <2> + testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' + } + + ext { <3> + snippetsDir = file('build/generated-snippets') + } + + test { <4> + systemProperty 'org.springframework.restdocs.outputDir', snippetsDir + outputs.dir snippetsDir + } + + asciidoctor { <5> + attributes 'snippets': snippetsDir + inputs.dir snippetsDir + dependsOn test + } +---- +<1> Apply the Asciidoctor plugin +<2> Add a dependency on spring-restdocs in the `testCompile` configuration: +<3> Configure a property to define the output location for generated snippets: +<4> Configure the `test` task with the `org.springframework.restdocs.outputDir` system +property. This property controls the location into which Spring REST Docs will write the +snippets that it generates. +<5> Configure the `asciidoctor` task and define an attribute named `snippets`. You can +then use this attribute when including the generated snippets in your documentation. + + + +[[getting-started-build-configuration-maven]] +==== Maven build configuration + +Both {samples}[sample applications] contain `pom.xml` files that you may wish to +use as a reference. The key parts of the configuration are described below. + + + +[source,xml,indent=0] +---- + <1> + org.springframework.restdocs + spring-restdocs + 0.1.0.BUILD-SNAPSHOT + test + + + <2> + ${project.build.directory}/generated-snippets + + + + + <3> + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + + + + ${snippetsDirectory} + + + + + <4> + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + package + + process-asciidoc + + + html + book + + ${snippetsDirectory} + + + + + + + + +---- +<1> Add a dependency on `spring-restdocs` in the `test` scope +<2> Configure a property to define the output location for generated snippets +<3> Configure the SureFire plugin with the `org.springframework.restdocs.outputDir` system +property. This property controls the location into which Spring REST docs will write the +snippets that it generates. The plugin is also configured to include files whose names end +with `Documentation.java`: +<4> Configure the Asciidoctor plugin and define an attribute named `snippets`. You can +then use this attribute when including the generated snippets in your documentation. + + +[[getting-started-documentation-snippets]] +=== Generating documentation snippets +Spring REST Docs uses {spring-mvc-test-docs}[Spring's MVC Test] to make requests to the +service that you are documenting. It then produces documentation snippets for the +result's request and response. + + + +[[getting-started-documentation-snippets-setup]] +==== Setting up Spring MVC test + +The first step in generating documentation snippets is to provide an `@Before` method +that creates a `MockMvc` instance: + +[source,java,indent=0] +---- +include::{examples-dir}com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +---- + +The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An instance +of this class can be obtained from the static `documentationConfiguration()` method on +`org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies +sensible defaults and also provides an API for customizing the configuration. Refer to the +<> for more information. + + + +[[getting-started-documentation-snippets-setup]] +==== Invoking the RESTful service + +Now that a `MockMvc` instance has been created, it can be used to invoke the RESTful +service and document the request and response. + +[source,java,indent=0] +---- +include::{examples-dir}com/example/InvokeService.java[tags=invoke-service] +---- +<1> Invoke the root (`/`) of the service an indicate that an `application/json` response +is required +<2> Assert that the service is produced the expected response +<3> Document the call to the service, writing the snippets into a directory named `index` +that will be located beneath the configured output directory. The snippets are written by +a `RestDocumentationResultHandler`. An instance of this class can be obtained from the +static `document` method on `org.springframework.restdocs.RestDocumentation`. + +By default, three snippets a written: + + * `/index/curl-request.adoc` + * `/index/http-request.adoc` + * `/index/http-response.adoc` + +Refer to <> for more information about these and other snippets +that can be produced by Spring REST Docs. + + + +[[getting-started-using-the-snippets]] +=== Using the snippets + +The generated snippets can be included in your documentation using the +http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. +The `snippets` attribute specified in the <> can be used to reference the snippets output directory, for example: + +[source,adoc,indent=0] +.... +[source,bash] +---- +\include::{snippets}/index/curl-request.adoc[] +---- +.... diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..f5f245c9f --- /dev/null +++ b/docs/src/docs/asciidoc/index.adoc @@ -0,0 +1,23 @@ += Spring REST Docs +Andy Wilkinson +:doctype: book +:toc: left +:toclevels: 3 +:source-highlighter: highlightjs +:spring-mvc-test-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html#spring-mvc-test-framework +:samples: https://github.com/spring-projects/spring-restdocs/tree/master/samples +:hateoas-sample: https://github.com/spring-projects/spring-restdocs/tree/master/samples/rest-notes-spring-hateoas +:data-rest-sample: https://github.com/spring-projects/spring-restdocs/tree/master/samples/rest-notes-spring-data-rest +:examples-dir: ../../test/java/ +:icons: font + +[[abstract]] + +Document RESTful services by combining hand-written documentation with auto-generated +snippets produced with Spring MVC Test. + +include::introduction.adoc[] +include::getting-started.adoc[] +include::documenting-your-api.adoc[] +include::customizing-responses.adoc[] +include::configuration.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc new file mode 100644 index 000000000..8fceeb5ed --- /dev/null +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -0,0 +1,23 @@ +[[introduction]] +== Introduction + +The aim of Spring REST Docs is to help you to produce documentation for your RESTful +services that is accurate and readable. + +Writing high-quality documentation is difficult. One way to ease that difficulty is to use +tools that are well-suited to the job. To this end, Spring REST Docs uses +http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produces +HTML styled and layed out to suit your needs. + +Spring REST Docs makes use of snippets produced by tests written with +{spring-mvc-test-docs}[Spring MVC Test]. This test-driven approach helps to guarantee the +accuracy of your service's documentation. If a snippet is incorrect the test that +produces it will fail. + +Documenting a RESTful service is largely about describing its resources. Two key parts +of each resource's description are the details of the HTTP requests that it consumes +and the HTTP responses that it produces. Spring REST Docs allows you to work with these +resources and the HTTP requests and responses, shielding your documentation +from the inner-details of your service's implementation. This separation helps you to +document your service's API rather than its implementation. It also frees you to evolve +the implementation without having to rework the documentation. \ No newline at end of file diff --git a/docs/src/test/java/com/example/AlwaysDo.java b/docs/src/test/java/com/example/AlwaysDo.java new file mode 100644 index 000000000..9962b217f --- /dev/null +++ b/docs/src/test/java/com/example/AlwaysDo.java @@ -0,0 +1,42 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class AlwaysDo { + + private MockMvc mockMvc; + + private WebApplicationContext context; + + // tag::always-do[] + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); + } + // end::always-do[] +} diff --git a/docs/src/test/java/com/example/CustomEncoding.java b/docs/src/test/java/com/example/CustomEncoding.java new file mode 100644 index 000000000..997ab9c32 --- /dev/null +++ b/docs/src/test/java/com/example/CustomEncoding.java @@ -0,0 +1,44 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class CustomEncoding { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + // tag::custom-encoding[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration().snippets() + .withEncoding("ISO-8859-1")) + .build(); + // end::custom-encoding[] + } + +} diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/CustomUriConfiguration.java new file mode 100644 index 000000000..845481d44 --- /dev/null +++ b/docs/src/test/java/com/example/CustomUriConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class CustomUriConfiguration { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + // tag::custom-uri-configuration[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration().uris() + .withScheme("https") + .withHost("example.com") + .withPort(443) + .withContextPath("/api")) + .build(); + // end::custom-uri-configuration[] + } + +} diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java new file mode 100644 index 000000000..2ac5f6b9e --- /dev/null +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -0,0 +1,42 @@ +/* + * 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 com.example; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; + +public class ExampleApplicationTests { + + // tag::mock-mvc-setup[] + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration()) + .build(); + } + // end::mock-mvc-setup[] +} \ No newline at end of file diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java new file mode 100644 index 000000000..8c69f2dbb --- /dev/null +++ b/docs/src/test/java/com/example/Hypermedia.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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.LinkExtractors.halLinks; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +public class Hypermedia { + + private MockMvc mockMvc; + + public void links() throws Exception { + // tag::links[] + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("index").withLinks( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), // <2> + linkWithRel("bravo").description("Link to the bravo resource"))); // <3> + // end::links[] + } + + public void explicitExtractor() throws Exception { + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + //tag::explicit-extractor[] + .andDo(document("index").withLinks(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource"))); + // end::explicit-extractor[] + } + + +} diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/InvokeService.java new file mode 100644 index 000000000..7a863eabd --- /dev/null +++ b/docs/src/test/java/com/example/InvokeService.java @@ -0,0 +1,38 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +public class InvokeService { + + private MockMvc mockMvc; + + public void invokeService() throws Exception { + // tag::invoke-service[] + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) // <1> + .andExpect(status().isOk()) // <2> + .andDo(document("index")); // <3> + // end::invoke-service[] + } + +} diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java new file mode 100644 index 000000000..ddfd75797 --- /dev/null +++ b/docs/src/test/java/com/example/Payload.java @@ -0,0 +1,54 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +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.result.MockMvcResultMatchers.status; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldType; +import org.springframework.test.web.servlet.MockMvc; + +public class Payload { + +private MockMvc mockMvc; + + public void response() throws Exception { + // tag::response[] + this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("index").withResponseFields( // <1> + fieldWithPath("contact").description("The user's contact details"), // <2> + fieldWithPath("contact.email").description("The user's email address"))); // <3> + // end::response[] + } + + public void explicitType() throws Exception { + this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + // tag::explicit-type[] + .andDo(document("index").withResponseFields( + fieldWithPath("contact.email") + .type(FieldType.STRING) // <1> + .optional() + .description("The user's email address"))); + // end::explicit-type[] + } + +} diff --git a/docs/src/test/java/com/example/QueryParameters.java b/docs/src/test/java/com/example/QueryParameters.java new file mode 100644 index 000000000..682d2387d --- /dev/null +++ b/docs/src/test/java/com/example/QueryParameters.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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.test.web.servlet.MockMvc; + +public class QueryParameters { + + private MockMvc mockMvc; + + public void queryParameters() throws Exception { + // tag::query-parameters[] + this.mockMvc.perform(get("/users?page=2&per_page=100")) + .andExpect(status().isOk()) + .andDo(document("users").withQueryParameters( // <1> + parameterWithName("page").description("The page to retrieve"), // <2> + parameterWithName("per_page").description("Entries per page") // <3> + )); + // end::query-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/ResponsePostProcessing.java b/docs/src/test/java/com/example/ResponsePostProcessing.java new file mode 100644 index 000000000..605b45343 --- /dev/null +++ b/docs/src/test/java/com/example/ResponsePostProcessing.java @@ -0,0 +1,38 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.test.web.servlet.MockMvc; + +public class ResponsePostProcessing { + + private MockMvc mockMvc; + + public void general() throws Exception { + // tag::general[] + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(modifyResponseTo(/* ... */) // <1> + .andDocument("index")); // <2> + // end::general[] + } + +} diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle index 0acf3af59..faa47de72 100644 --- a/samples/rest-notes-spring-data-rest/build.gradle +++ b/samples/rest-notes-spring-data-rest/build.gradle @@ -39,18 +39,18 @@ dependencies { } ext { - generatedDocumentation = file('build/generated-snippets') + snippetsDir = file('build/generated-snippets') } test { - systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation - outputs.dir generatedDocumentation + systemProperty 'org.springframework.restdocs.outputDir', snippetsDir + outputs.dir snippetsDir } asciidoctor { sourceDir 'src/main/asciidoc' // Align with Maven's default location - attributes 'generated': generatedDocumentation - inputs.dir generatedDocumentation + attributes 'snippets': snippetsDir + inputs.dir snippetsDir dependsOn test } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index a12bcf064..9ccf53f5e 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,6 +18,7 @@ UTF-8 1.7 + ${project.build.directory}/generated-snippets @@ -68,7 +69,7 @@ **/*Documentation.java - ${project.build.directory}/generated-snippets + ${snippetsDirectory} @@ -87,7 +88,7 @@ html book - ${project.build.directory}/generated-snippets + ${snippetsDirectory} diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 9b14b377c..58cb3ad5b 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -64,12 +64,12 @@ use of HTTP status codes. 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 structure: -include::{generated}/error-example/response-fields.adoc[] +include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/http-response.adoc[] +include::{snippets}/error-example/http-response.adoc[] [[overview-hypermedia]] == Hypermedia @@ -99,18 +99,18 @@ A `GET` request is used to access the index ==== Response structure -include::{generated}/index-example/response-fields.adoc[] +include::{snippets}/index-example/response-fields.adoc[] ==== Example response -include::{generated}/index-example/http-response.adoc[] +include::{snippets}/index-example/http-response.adoc[] [[resources-index-links]] ==== Links -include::{generated}/index-example/links.adoc[] +include::{snippets}/index-example/links.adoc[] @@ -128,15 +128,15 @@ A `GET` request will list all of the service's notes. ==== Response structure -include::{generated}/notes-list-example/response-fields.adoc[] +include::{snippets}/notes-list-example/response-fields.adoc[] ==== Example request -include::{generated}/notes-list-example/curl-request.adoc[] +include::{snippets}/notes-list-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-list-example/http-response.adoc[] +include::{snippets}/notes-list-example/http-response.adoc[] @@ -147,15 +147,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/notes-create-example/request-fields.adoc[] +include::{snippets}/notes-create-example/request-fields.adoc[] ==== Example request -include::{generated}/notes-create-example/curl-request.adoc[] +include::{snippets}/notes-create-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-create-example/http-response.adoc[] +include::{snippets}/notes-create-example/http-response.adoc[] @@ -173,15 +173,15 @@ A `GET` request will list all of the service's tags. ==== Response structure -include::{generated}/tags-list-example/response-fields.adoc[] +include::{snippets}/tags-list-example/response-fields.adoc[] ==== Example request -include::{generated}/tags-list-example/curl-request.adoc[] +include::{snippets}/tags-list-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-list-example/http-response.adoc[] +include::{snippets}/tags-list-example/http-response.adoc[] @@ -192,15 +192,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/tags-create-example/request-fields.adoc[] +include::{snippets}/tags-create-example/request-fields.adoc[] ==== Example request -include::{generated}/tags-create-example/curl-request.adoc[] +include::{snippets}/tags-create-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-create-example/http-response.adoc[] +include::{snippets}/tags-create-example/http-response.adoc[] @@ -214,7 +214,7 @@ The Note resource is used to retrieve, update, and delete individual notes [[resources-note-links]] === Links -include::{generated}/note-get-example/links.adoc[] +include::{snippets}/note-get-example/links.adoc[] @@ -225,15 +225,15 @@ A `GET` request will retrieve the details of a note ==== Response structure -include::{generated}/note-get-example/response-fields.adoc[] +include::{snippets}/note-get-example/response-fields.adoc[] ==== Example request -include::{generated}/note-get-example/curl-request.adoc[] +include::{snippets}/note-get-example/curl-request.adoc[] ==== Example response -include::{generated}/note-get-example/http-response.adoc[] +include::{snippets}/note-get-example/http-response.adoc[] @@ -244,17 +244,17 @@ A `PATCH` request is used to update a note ==== Request structure -include::{generated}/note-update-example/request-fields.adoc[] +include::{snippets}/note-update-example/request-fields.adoc[] To leave an attribute of a note unchanged, any of the above may be omitted from the request. ==== Example request -include::{generated}/note-update-example/curl-request.adoc[] +include::{snippets}/note-update-example/curl-request.adoc[] ==== Example response -include::{generated}/note-update-example/http-response.adoc[] +include::{snippets}/note-update-example/http-response.adoc[] @@ -268,7 +268,7 @@ The Tag resource is used to retrieve, update, and delete individual tags [[resources-tag-links]] === Links -include::{generated}/tag-get-example/links.adoc[] +include::{snippets}/tag-get-example/links.adoc[] @@ -279,15 +279,15 @@ A `GET` request will retrieve the details of a tag ==== Response structure -include::{generated}/tag-get-example/response-fields.adoc[] +include::{snippets}/tag-get-example/response-fields.adoc[] ==== Example request -include::{generated}/tag-get-example/curl-request.adoc[] +include::{snippets}/tag-get-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-get-example/http-response.adoc[] +include::{snippets}/tag-get-example/http-response.adoc[] @@ -298,12 +298,12 @@ A `PATCH` request is used to update a tag ==== Request structure -include::{generated}/tag-update-example/request-fields.adoc[] +include::{snippets}/tag-update-example/request-fields.adoc[] ==== Example request -include::{generated}/tag-update-example/curl-request.adoc[] +include::{snippets}/tag-update-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-update-example/http-response.adoc[] +include::{snippets}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc index f9d9788c0..a6678bb55 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc @@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/curl-request.adoc[] +include::{snippets}/index/1/curl-request.adoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] To create a note, you need to execute a `POST` request to this URI including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/curl-request.adoc[] +include::{snippets}/creating-a-note/1/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/http-response.adoc[] +include::{snippets}/creating-a-note/1/http-response.adoc[] To work with the newly created note you use the URI in the `Location` header. For example, you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/curl-request.adoc[] +include::{snippets}/creating-a-note/2/curl-request.adoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/http-response.adoc[] +include::{snippets}/creating-a-note/2/http-response.adoc[] Note the `tags` link which we'll make use of later. @@ -92,26 +92,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/curl-request.adoc[] +include::{snippets}/creating-a-note/3/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/http-response.adoc[] +include::{snippets}/creating-a-note/3/http-response.adoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/curl-request.adoc[] +include::{snippets}/creating-a-note/4/curl-request.adoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/http-response.adoc[] +include::{snippets}/creating-a-note/4/http-response.adoc[] @@ -132,25 +132,25 @@ with it. Once again we execute a `POST` request. However, this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/curl-request.adoc[] +include::{snippets}/creating-a-note/5/curl-request.adoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/creating-a-note/5/http-response.adoc[] +include::{snippets}/creating-a-note/5/http-response.adoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/curl-request.adoc[] -include::{generated}/creating-a-note/6/http-response.adoc[] +include::{snippets}/creating-a-note/6/curl-request.adoc[] +include::{snippets}/creating-a-note/6/http-response.adoc[] To verify that the tag has been associated with the note, we can perform a `GET` request against the URI from the `tags` link: -include::{generated}/creating-a-note/7/curl-request.adoc[] +include::{snippets}/creating-a-note/7/curl-request.adoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/creating-a-note/7/http-response.adoc[] +include::{snippets}/creating-a-note/7/http-response.adoc[] @@ -160,18 +160,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll used the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/curl-request.adoc[] +include::{snippets}/creating-a-note/8/curl-request.adoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/http-response.adoc[] +include::{snippets}/creating-a-note/8/http-response.adoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/creating-a-note/2/http-response.adoc[] +include::{snippets}/creating-a-note/2/http-response.adoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/curl-request.adoc[] -include::{generated}/creating-a-note/9/http-response.adoc[] +include::{snippets}/creating-a-note/9/curl-request.adoc[] +include::{snippets}/creating-a-note/9/http-response.adoc[] diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index d8b78d536..f1a6120c2 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -41,18 +41,18 @@ dependencies { } ext { - generatedDocumentation = file('build/generated-snippets') + snippetsDir = file('build/generated-snippets') } test { - systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation - outputs.dir generatedDocumentation + systemProperty 'org.springframework.restdocs.outputDir', snippetsDir + outputs.dir snippetsDir } asciidoctor { sourceDir 'src/main/asciidoc' // Align with Maven's default location - attributes 'generated': generatedDocumentation - inputs.dir generatedDocumentation + attributes 'snippets': snippetsDir + inputs.dir snippetsDir dependsOn test } diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml index 7484e4852..52127a160 100644 --- a/samples/rest-notes-spring-hateoas/pom.xml +++ b/samples/rest-notes-spring-hateoas/pom.xml @@ -18,6 +18,7 @@ UTF-8 1.7 + ${project.build.directory}/generated-snippets @@ -84,7 +85,7 @@ **/*Documentation.java - ${project.build.directory}/generated-snippets + ${snippetsDirectory} @@ -103,7 +104,7 @@ html book - ${project.build.directory}/generated-snippets + ${snippetsDirectory} diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 71227a702..bf6c25ea8 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -64,12 +64,12 @@ use of HTTP status codes. 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 structure: -include::{generated}/error-example/response-fields.adoc[] +include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/http-response.adoc[] +include::{snippets}/error-example/http-response.adoc[] [[overview-hypermedia]] == Hypermedia @@ -99,18 +99,18 @@ A `GET` request is used to access the index ==== Response structure -include::{generated}/index-example/response-fields.adoc[] +include::{snippets}/index-example/response-fields.adoc[] ==== Example response -include::{generated}/index-example/http-response.adoc[] +include::{snippets}/index-example/http-response.adoc[] [[resources-index-links]] ==== Links -include::{generated}/index-example/links.adoc[] +include::{snippets}/index-example/links.adoc[] @@ -128,15 +128,15 @@ A `GET` request will list all of the service's notes. ==== Response structure -include::{generated}/notes-list-example/response-fields.adoc[] +include::{snippets}/notes-list-example/response-fields.adoc[] ==== Example request -include::{generated}/notes-list-example/curl-request.adoc[] +include::{snippets}/notes-list-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-list-example/http-response.adoc[] +include::{snippets}/notes-list-example/http-response.adoc[] @@ -147,15 +147,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/notes-create-example/request-fields.adoc[] +include::{snippets}/notes-create-example/request-fields.adoc[] ==== Example request -include::{generated}/notes-create-example/curl-request.adoc[] +include::{snippets}/notes-create-example/curl-request.adoc[] ==== Example response -include::{generated}/notes-create-example/http-response.adoc[] +include::{snippets}/notes-create-example/http-response.adoc[] @@ -173,15 +173,15 @@ A `GET` request will list all of the service's tags. ==== Response structure -include::{generated}/tags-list-example/response-fields.adoc[] +include::{snippets}/tags-list-example/response-fields.adoc[] ==== Example request -include::{generated}/tags-list-example/curl-request.adoc[] +include::{snippets}/tags-list-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-list-example/http-response.adoc[] +include::{snippets}/tags-list-example/http-response.adoc[] @@ -192,15 +192,15 @@ A `POST` request is used to create a note ==== Request structure -include::{generated}/tags-create-example/request-fields.adoc[] +include::{snippets}/tags-create-example/request-fields.adoc[] ==== Example request -include::{generated}/tags-create-example/curl-request.adoc[] +include::{snippets}/tags-create-example/curl-request.adoc[] ==== Example response -include::{generated}/tags-create-example/http-response.adoc[] +include::{snippets}/tags-create-example/http-response.adoc[] @@ -214,7 +214,7 @@ The Note resource is used to retrieve, update, and delete individual notes [[resources-note-links]] === Links -include::{generated}/note-get-example/links.adoc[] +include::{snippets}/note-get-example/links.adoc[] @@ -225,15 +225,15 @@ A `GET` request will retrieve the details of a note ==== Response structure -include::{generated}/note-get-example/response-fields.adoc[] +include::{snippets}/note-get-example/response-fields.adoc[] ==== Example request -include::{generated}/note-get-example/curl-request.adoc[] +include::{snippets}/note-get-example/curl-request.adoc[] ==== Example response -include::{generated}/note-get-example/http-response.adoc[] +include::{snippets}/note-get-example/http-response.adoc[] @@ -244,17 +244,17 @@ A `PATCH` request is used to update a note ==== Request structure -include::{generated}/note-update-example/request-fields.adoc[] +include::{snippets}/note-update-example/request-fields.adoc[] To leave an attribute of a note unchanged, any of the above may be omitted from the request. ==== Example request -include::{generated}/note-update-example/curl-request.adoc[] +include::{snippets}/note-update-example/curl-request.adoc[] ==== Example response -include::{generated}/note-update-example/http-response.adoc[] +include::{snippets}/note-update-example/http-response.adoc[] [[resources-tag]] @@ -267,7 +267,7 @@ The Tag resource is used to retrieve, update, and delete individual tags [[resources-tag-links]] === Links -include::{generated}/tag-get-example/links.adoc[] +include::{snippets}/tag-get-example/links.adoc[] @@ -278,15 +278,15 @@ A `GET` request will retrieve the details of a tag ==== Response structure -include::{generated}/tag-get-example/response-fields.adoc[] +include::{snippets}/tag-get-example/response-fields.adoc[] ==== Example request -include::{generated}/tag-get-example/curl-request.adoc[] +include::{snippets}/tag-get-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-get-example/http-response.adoc[] +include::{snippets}/tag-get-example/http-response.adoc[] @@ -297,12 +297,12 @@ A `PATCH` request is used to update a tag ==== Request structure -include::{generated}/tag-update-example/request-fields.adoc[] +include::{snippets}/tag-update-example/request-fields.adoc[] ==== Example request -include::{generated}/tag-update-example/curl-request.adoc[] +include::{snippets}/tag-update-example/curl-request.adoc[] ==== Example response -include::{generated}/tag-update-example/http-response.adoc[] +include::{snippets}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc index 93b37296c..676a4615c 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/curl-request.adoc[] +include::{snippets}/index/1/curl-request.adoc[] This request should yield the following response: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] To create a note you need to execute a `POST` request to this URI, including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/curl-request.adoc[] +include::{snippets}/creating-a-note/1/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/http-response.adoc[] +include::{snippets}/creating-a-note/1/http-response.adoc[] To work with the newly created note you use the URI in the `Location` header. For example you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/curl-request.adoc[] +include::{snippets}/creating-a-note/2/curl-request.adoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/http-response.adoc[] +include::{snippets}/creating-a-note/2/http-response.adoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/http-response.adoc[] +include::{snippets}/index/1/http-response.adoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/curl-request.adoc[] +include::{snippets}/creating-a-note/3/curl-request.adoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/http-response.adoc[] +include::{snippets}/creating-a-note/3/http-response.adoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/curl-request.adoc[] +include::{snippets}/creating-a-note/4/curl-request.adoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/http-response.adoc[] +include::{snippets}/creating-a-note/4/http-response.adoc[] @@ -131,25 +131,25 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/curl-request.adoc[] +include::{snippets}/creating-a-note/5/curl-request.adoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/creating-a-note/5/http-response.adoc[] +include::{snippets}/creating-a-note/5/http-response.adoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/curl-request.adoc[] -include::{generated}/creating-a-note/6/http-response.adoc[] +include::{snippets}/creating-a-note/6/curl-request.adoc[] +include::{snippets}/creating-a-note/6/http-response.adoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/creating-a-note/7/curl-request.adoc[] +include::{snippets}/creating-a-note/7/curl-request.adoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/creating-a-note/7/http-response.adoc[] +include::{snippets}/creating-a-note/7/http-response.adoc[] @@ -159,18 +159,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/curl-request.adoc[] +include::{snippets}/creating-a-note/8/curl-request.adoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/http-response.adoc[] +include::{snippets}/creating-a-note/8/http-response.adoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/creating-a-note/2/http-response.adoc[] +include::{snippets}/creating-a-note/2/http-response.adoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/curl-request.adoc[] -include::{generated}/creating-a-note/9/http-response.adoc[] +include::{snippets}/creating-a-note/9/curl-request.adoc[] +include::{snippets}/creating-a-note/9/http-response.adoc[] diff --git a/settings.gradle b/settings.gradle index 80e754ca7..ad08f6b31 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'spring-restdocs-build' +include 'docs' include 'spring-restdocs' \ No newline at end of file diff --git a/spring-restdocs/build.gradle b/spring-restdocs/build.gradle new file mode 100644 index 000000000..0cca8f028 --- /dev/null +++ b/spring-restdocs/build.gradle @@ -0,0 +1,89 @@ +ext { + hamcrestVersion = '1.3' + jacocoVersion = '0.7.2.201409121644' +} + +group = 'org.springframework.restdocs' + +apply plugin: 'maven' +apply plugin: 'sonar-runner' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } + +repositories { + jcenter() +} + +sonarRunner { + sonarProperties { + property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" + property 'sonar.java.coveragePlugin', 'jacoco' + property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' + property 'sonar.links.homepage', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.issue', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.scm', 'https://github.com/spring-projects/spring-restdocs' + } +} + +configurations { + jacoco +} + +ext { + javadocLinks = [ + 'http://docs.oracle.com/javase/8/docs/api/', + "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/" + ] as String[] +} + +dependencies { + compile 'com.fasterxml.jackson.core:jackson-databind' + compile 'junit:junit' + compile 'org.springframework:spring-core' + compile 'org.springframework:spring-test' + compile 'org.springframework:spring-web' + compile 'org.springframework:spring-webmvc' + compile 'javax.servlet:javax.servlet-api' + jacoco 'org.jacoco:org.jacoco.agent::runtime' + testCompile 'org.springframework.hateoas:spring-hateoas' + testCompile 'org.mockito:mockito-core' + testCompile 'org.hamcrest:hamcrest-core' + testCompile 'org.hamcrest:hamcrest-library' +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from project.sourceSets.main.allSource +} + +javadoc { + description = "Generates project-level javadoc for use in -javadoc jar" + + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = "Spring REST Docs $version" + options.docTitle = "${options.header} API" + options.links(project.ext.javadocLinks) + options.addStringOption('-quiet') +} + +task javadocJar(type: Jar) { + classifier = "javadoc" + from javadoc +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" + testLogging { + exceptionFormat "full" + } +} \ No newline at end of file From 833f0eabb84f957e6ca285694ab7f676d6c355d9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jun 2015 17:21:43 +0100 Subject: [PATCH 0074/1059] Polishing: replace spaces with tabs in doc examples See gh-37 --- docs/src/test/java/com/example/AlwaysDo.java | 12 ++++++------ docs/src/test/java/com/example/CustomEncoding.java | 6 +++--- .../java/com/example/CustomUriConfiguration.java | 6 +++--- .../java/com/example/ExampleApplicationTests.java | 6 +++--- docs/src/test/java/com/example/Hypermedia.java | 10 +++++----- docs/src/test/java/com/example/InvokeService.java | 4 ++-- docs/src/test/java/com/example/Payload.java | 6 +++--- docs/src/test/java/com/example/QueryParameters.java | 4 ++-- .../java/com/example/ResponsePostProcessing.java | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/src/test/java/com/example/AlwaysDo.java b/docs/src/test/java/com/example/AlwaysDo.java index 9962b217f..d4575b9a7 100644 --- a/docs/src/test/java/com/example/AlwaysDo.java +++ b/docs/src/test/java/com/example/AlwaysDo.java @@ -25,18 +25,18 @@ import org.springframework.web.context.WebApplicationContext; public class AlwaysDo { - + private MockMvc mockMvc; - + private WebApplicationContext context; // tag::always-do[] @Before public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()) - .alwaysDo(document("{method-name}/{step}/")) - .build(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } // end::always-do[] } diff --git a/docs/src/test/java/com/example/CustomEncoding.java b/docs/src/test/java/com/example/CustomEncoding.java index 997ab9c32..1c72fcff0 100644 --- a/docs/src/test/java/com/example/CustomEncoding.java +++ b/docs/src/test/java/com/example/CustomEncoding.java @@ -25,12 +25,12 @@ import org.springframework.web.context.WebApplicationContext; public class CustomEncoding { - + @Autowired private WebApplicationContext context; - + private MockMvc mockMvc; - + @Before public void setUp() { // tag::custom-encoding[] diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/CustomUriConfiguration.java index 845481d44..265e414be 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/CustomUriConfiguration.java @@ -25,12 +25,12 @@ import org.springframework.web.context.WebApplicationContext; public class CustomUriConfiguration { - + @Autowired private WebApplicationContext context; - + private MockMvc mockMvc; - + @Before public void setUp() { // tag::custom-uri-configuration[] diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java index 2ac5f6b9e..e6c97e444 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -25,13 +25,13 @@ import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; public class ExampleApplicationTests { - + // tag::mock-mvc-setup[] @Autowired private WebApplicationContext context; - + private MockMvc mockMvc; - + @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java index 8c69f2dbb..a9e0de175 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -26,9 +26,9 @@ import org.springframework.test.web.servlet.MockMvc; public class Hypermedia { - + private MockMvc mockMvc; - + public void links() throws Exception { // tag::links[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) @@ -38,16 +38,16 @@ public void links() throws Exception { linkWithRel("bravo").description("Link to the bravo resource"))); // <3> // end::links[] } - + public void explicitExtractor() throws Exception { this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) //tag::explicit-extractor[] .andDo(document("index").withLinks(halLinks(), // <1> - linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("alpha").description("Link to the alpha resource"), linkWithRel("bravo").description("Link to the bravo resource"))); // end::explicit-extractor[] } - + } diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/InvokeService.java index 7a863eabd..18fc85088 100644 --- a/docs/src/test/java/com/example/InvokeService.java +++ b/docs/src/test/java/com/example/InvokeService.java @@ -26,7 +26,7 @@ public class InvokeService { private MockMvc mockMvc; - + public void invokeService() throws Exception { // tag::invoke-service[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) // <1> @@ -34,5 +34,5 @@ public void invokeService() throws Exception { .andDo(document("index")); // <3> // end::invoke-service[] } - + } diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index ddfd75797..bec0f361d 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -26,9 +26,9 @@ import org.springframework.test.web.servlet.MockMvc; public class Payload { - + private MockMvc mockMvc; - + public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) @@ -38,7 +38,7 @@ public void response() throws Exception { fieldWithPath("contact.email").description("The user's email address"))); // <3> // end::response[] } - + public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) diff --git a/docs/src/test/java/com/example/QueryParameters.java b/docs/src/test/java/com/example/QueryParameters.java index 682d2387d..ca81142c0 100644 --- a/docs/src/test/java/com/example/QueryParameters.java +++ b/docs/src/test/java/com/example/QueryParameters.java @@ -26,7 +26,7 @@ public class QueryParameters { private MockMvc mockMvc; - + public void queryParameters() throws Exception { // tag::query-parameters[] this.mockMvc.perform(get("/users?page=2&per_page=100")) @@ -37,5 +37,5 @@ public void queryParameters() throws Exception { )); // end::query-parameters[] } - + } diff --git a/docs/src/test/java/com/example/ResponsePostProcessing.java b/docs/src/test/java/com/example/ResponsePostProcessing.java index 605b45343..c9a6bcb01 100644 --- a/docs/src/test/java/com/example/ResponsePostProcessing.java +++ b/docs/src/test/java/com/example/ResponsePostProcessing.java @@ -25,7 +25,7 @@ public class ResponsePostProcessing { private MockMvc mockMvc; - + public void general() throws Exception { // tag::general[] this.mockMvc.perform(get("/")) @@ -34,5 +34,5 @@ public void general() throws Exception { .andDocument("index")); // <2> // end::general[] } - + } From e5840cd48f2be3f1ed2519833e52e23b900f277d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jun 2015 17:30:09 +0100 Subject: [PATCH 0075/1059] Set the group on the root project so the docs are published correctly See gh-37 --- build.gradle | 4 ++++ spring-restdocs/build.gradle | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4f1507248..3617a1407 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,10 @@ buildscript { } } +allprojects { + group = 'org.springframework.restdocs' +} + apply plugin: 'samples' ext { diff --git a/spring-restdocs/build.gradle b/spring-restdocs/build.gradle index 0cca8f028..65056ff47 100644 --- a/spring-restdocs/build.gradle +++ b/spring-restdocs/build.gradle @@ -3,8 +3,6 @@ ext { jacocoVersion = '0.7.2.201409121644' } -group = 'org.springframework.restdocs' - apply plugin: 'maven' apply plugin: 'sonar-runner' From 0e5151e91bea5a85ed6539fea03ce6334f85cf36 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jun 2015 19:55:41 +0100 Subject: [PATCH 0076/1059] Bump the version to 1.0.0.BUILD-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8c4851838..83e39c4af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.1.0.BUILD-SNAPSHOT +version=1.0.0.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 0699cdd068f042aee98ea454b343c4cae18fad73 Mon Sep 17 00:00:00 2001 From: Kellen Dye Date: Wed, 10 Jun 2015 15:48:57 +0200 Subject: [PATCH 0077/1059] Allow links to be optional Previously, if a link was documented then it had to be present in the response payload for the test to pass. This was unnecessarily restrictive and caused problems when a link was omitted due to the application's current state. This commit allows a link to be declared as optional. If a link is optional then the test will pass irrespective of the link's presence in the response. See gh-74 --- .../restdocs/hypermedia/LinkDescriptor.java | 16 ++++++++++++++ .../hypermedia/LinkSnippetResultHandler.java | 12 ++++++----- .../HypermediaDocumentationTests.java | 21 ++++++++++++++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 24a2044cc..dca8adcb2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -29,6 +29,8 @@ public class LinkDescriptor { private String description; + private boolean optional; + LinkDescriptor(String rel) { this.rel = rel; } @@ -44,6 +46,16 @@ public LinkDescriptor description(String description) { return this; } + /** + * Marks the link as optional + * + * @return {@code this} + */ + public LinkDescriptor optional() { + this.optional = true; + return this; + } + String getRel() { return this.rel; } @@ -51,4 +63,8 @@ String getRel() { String getDescription() { return this.description; } + + boolean isOptional() { + return this.optional; + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 5bd8cbe7e..033364cd5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -42,6 +42,8 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { private final Map descriptorsByRel = new LinkedHashMap<>(); + private final Set requiredRels = new HashSet(); + private final LinkExtractor extractor; LinkSnippetResultHandler(String outputDir, LinkExtractor linkExtractor, @@ -52,6 +54,9 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { Assert.hasText(descriptor.getRel()); Assert.hasText(descriptor.getDescription()); this.descriptorsByRel.put(descriptor.getRel(), descriptor); + if (!descriptor.isOptional()) { + this.requiredRels.add(descriptor.getRel()); + } } } @@ -78,12 +83,11 @@ protected void handle(MvcResult result, DocumentationWriter writer) } Set actualRels = links.keySet(); - Set expectedRels = this.descriptorsByRel.keySet(); Set undocumentedRels = new HashSet(actualRels); - undocumentedRels.removeAll(expectedRels); + undocumentedRels.removeAll(this.descriptorsByRel.keySet()); - Set missingRels = new HashSet(expectedRels); + Set missingRels = new HashSet(this.requiredRels); missingRels.removeAll(actualRels); if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { @@ -102,8 +106,6 @@ protected void handle(MvcResult result, DocumentationWriter writer) throw new SnippetGenerationException(message); } - Assert.isTrue(actualRels.equals(expectedRels)); - writer.table(new TableAction() { @Override diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 8f7df315a..56588a062 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -60,10 +60,29 @@ public void missingLink() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - documentLinks("undocumented-link", new StubLinkExtractor(), + documentLinks("missing-link", new StubLinkExtractor(), new LinkDescriptor("foo").description("bar")).handle(result()); } + @Test + public void documentedOptionalLink() throws IOException { + this.snippet.expectLinks("documented-optional-link").withContents( // + tableWithHeader("Relation", "Description") // + .row("foo", "bar")); + documentLinks("documented-optional-link", + new StubLinkExtractor().withLinks(new Link("foo", "blah")), + new LinkDescriptor("foo").description("bar").optional()).handle(result()); + } + + @Test + public void missingOptionalLink() throws IOException { + this.snippet.expectLinks("missing-optional-link").withContents( // + tableWithHeader("Relation", "Description") // + .row("foo", "bar")); + documentLinks("missing-optional-link", new StubLinkExtractor(), + new LinkDescriptor("foo").description("bar").optional()).handle(result()); + } + @Test public void undocumentedLinkAndMissingLink() throws IOException { this.thrown.expect(SnippetGenerationException.class); From aeb88eb3139b22fe5cc0513ab9f45676fdcadbf5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jun 2015 13:44:26 +0100 Subject: [PATCH 0078/1059] Update the documentation to reflect that links can now be optional Closes gh-74 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index dad31532b..c8db048af 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -25,7 +25,7 @@ links. When documenting links, the test will fail if an undocumented link is found in the response. Similarly, the test will also fail if a documented link is not found in the -response. +response and the link has not be marked as optional. From 7cdbf0cea9ec24866f2f49473e0ce2d98300d7b9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 23 Jun 2015 13:54:05 +0100 Subject: [PATCH 0079/1059] Add a section to the docs that provides tips on working with Asciidoctor Closes gh-75 --- docs/src/docs/asciidoc/getting-started.adoc | 3 - docs/src/docs/asciidoc/index.adoc | 3 +- .../asciidoc/working-with-asciidoctor.adoc | 75 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 docs/src/docs/asciidoc/working-with-asciidoctor.adoc diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 614036d68..1f4602b44 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -211,9 +211,6 @@ The `snippets` attribute specified in the <> can be used to reference the snippets output directory, for example: [source,adoc,indent=0] -.... -[source,bash] ---- \include::{snippets}/index/curl-request.adoc[] ---- -.... diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index f5f245c9f..784fed0f4 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -20,4 +20,5 @@ include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] -include::configuration.adoc[] \ No newline at end of file +include::configuration.adoc[] +include::working-with-asciidoctor.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc new file mode 100644 index 000000000..33824f456 --- /dev/null +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -0,0 +1,75 @@ +[[working-with-asciidoctor]] +== Working with Asciidoctor + +This section describes any aspects of working with Asciidoctor that are particularly +relevant to Spring REST Docs. + + + +[[working-with-asciidoctor-resources]] +=== Resources + + * http://asciidoctor.org/docs/asciidoc-syntax-quick-reference[Syntax quick reference] + * http://asciidoctor.org/docs/user-manual[User manual] + + + +[[working-with-asciidoctor-including-snippets]] +=== Including snippets + +The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include +macro] is used to include generated snippets in your documentation. The `snippets` +attribute specified in the <> +can be used to reference the snippets output directory, for example: + +[source,adoc,indent=0] +---- +\include::{snippets}/index/curl-request.adoc[] +---- + + + +[[working-with-asciidoctor-customizing-tables]] +=== Customizing tables + +Many of the snippets contain a table in its default configuration. The appearance of the +table can be customized by providing some additional configuration when the snippet is +included. + + + +[[working-with-asciidoctor-customizing-tables-formatting-columns]] +==== Formatting columns + +Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format +[formatting a table's columns]. For example, the widths of a table's columns can be +specified using the `cols` attribute: + +[source,adoc,indent=0] +---- +[cols=1,3] <1> +\include::{snippets}index/links.adoc[] +---- +<1> The table's width will be split across its two columns with the second column being +three times as wide as the first. + + + +[[working-with-asciidoctor-customizing-tables-title]] +==== Configuring the title + +The title of a table can be specified using a line prefixed by a `.`: + +[source,adoc,indent=0] +---- +.Links <1> +\include::{snippets}index/links.adoc[] +---- +<1> The table's title will be `Links`. + + + +==== Further reading + +Refer to the http://asciidoctor.org/docs/user-manual/#tables[Tables section of +the Asciidoctor user manual] for more information about customizing tables. \ No newline at end of file From b6c64dfd26a6a653a674c3f17d1b42f1ed0a0043 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 23 Jun 2015 14:05:07 +0100 Subject: [PATCH 0080/1059] Update the samples to build against correct version of main project This should have been done as part of 0e5151e91. --- samples/rest-notes-spring-data-rest/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/rest-notes-spring-hateoas/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle index faa47de72..bd0b39c31 100644 --- a/samples/rest-notes-spring-data-rest/build.gradle +++ b/samples/rest-notes-spring-data-rest/build.gradle @@ -34,7 +34,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:1.0.0.BUILD-SNAPSHOT' } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 9ccf53f5e..e8a61e183 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -50,7 +50,7 @@ org.springframework.restdocs spring-restdocs - 0.1.0.BUILD-SNAPSHOT + 1.0.0.BUILD-SNAPSHOT test diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index f1a6120c2..249c02481 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -37,7 +37,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:1.0.0.BUILD-SNAPSHOT' } ext { diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml index 52127a160..350d0a72c 100644 --- a/samples/rest-notes-spring-hateoas/pom.xml +++ b/samples/rest-notes-spring-hateoas/pom.xml @@ -66,7 +66,7 @@ org.springframework.restdocs spring-restdocs - 0.1.0.BUILD-SNAPSHOT + 1.0.0.BUILD-SNAPSHOT test From 9824e8a198b25af412125be7e52de10f8ea47b2d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 23 Jun 2015 18:00:37 +0100 Subject: [PATCH 0081/1059] Include the version in the documentation's header --- docs/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/build.gradle b/docs/build.gradle index f075eb283..da84abcb1 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -16,4 +16,5 @@ asciidoctor { sources { include 'index.adoc' } + attributes 'revnumber': project.version } \ No newline at end of file From 1ae014dc53dad7d63d687d0bc9b98a616ac5625f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jun 2015 09:38:58 +0100 Subject: [PATCH 0082/1059] Update build script examples in docs to use dynamic dependency version --- docs/src/docs/asciidoc/getting-started.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 1f4602b44..8d58f1f43 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -38,14 +38,14 @@ use as a reference. The key parts of the configuration are described below. -[source,groovy,indent=0] +[source,groovy,indent=0,subs="verbatim,attributes"] ---- plugins { <1> id "org.asciidoctor.convert" version "1.5.2" } dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs:{project-version}' } ext { <3> @@ -82,12 +82,12 @@ use as a reference. The key parts of the configuration are described below. -[source,xml,indent=0] +[source,xml,indent=0,subs="verbatim,attributes"] ---- <1> org.springframework.restdocs spring-restdocs - 0.1.0.BUILD-SNAPSHOT + {project-version} test From 8a0b07576d5fc43ca1092a43ab53377305f671df Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jun 2015 11:45:30 +0100 Subject: [PATCH 0083/1059] Update README and reference docs to avoid duplication of content --- CONTRIBUTING.md | 7 +- README.md | 420 ++---------------- docs/build.gradle | 3 +- docs/src/docs/asciidoc/configuration.adoc | 4 +- docs/src/docs/asciidoc/contributing.adoc | 36 ++ docs/src/docs/asciidoc/getting-started.adoc | 96 +++- docs/src/docs/asciidoc/index.adoc | 19 +- docs/src/docs/asciidoc/introduction.adoc | 6 +- .../asciidoc/working-with-asciidoctor.adoc | 6 +- 9 files changed, 175 insertions(+), 422 deletions(-) create mode 100644 docs/src/docs/asciidoc/contributing.adoc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7b7c8d23..d58b56368 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Spring REST Docs -Spring REST docs is released under the Apache 2.0 license. If you would like to +Spring REST Docs is released under the Apache 2.0 license. If you would like to contribute something, or simply want to work with the code, this document should help you to get started. @@ -36,10 +36,7 @@ None of these is essential for a pull request, but they will all help ### Building from source -To build the source you will need to use Java 8 – the main project only requires Java 7 -but the sample projects use Java 8. - -The code can be built with Gradle: +To build the source you will need Java 7 or later. The code is built with Gradle: ``` $ ./gradlew build diff --git a/README.md b/README.md index c59611158..8ca99f410 100644 --- a/README.md +++ b/README.md @@ -1,407 +1,47 @@ -# Spring REST docs [![Build status][10]][11] +# Spring REST Docs [![Build status][1]][2] + +## Overview The primary goal of this project is to make it easy to document RESTful services by -combining content that's been hand-written with auto-generated examples produced -with the [Spring MVC Test][2] framework. The result is intended to be an easy-to-read -user guide, akin to [GitHub's API documentation][3] for example, rather than the fully -automated, dense API documentation produced by tools like [Swagger][4]. +combining content that's been hand-written using [Asciidoctor][3] with auto-generated +examples produced with the [Spring MVC Test][4] framework. The result is intended to be +an easy-to-read user guide, akin to [GitHub's API documentation][5] for example, rather +than the fully automated, dense API documentation produced by tools like [Swagger][6]. For a broader introduction see the Documenting RESTful APIs presentation. Both the -[slides][9] and a [video recording][13] are available. - -## Quickstart - -The project requires Spring Framework 4.1 and Java 7 or later. Snapshots are published to -`https://repo.spring.io/snapshot`. Alternatively, it can be built locally using Gradle: - -``` -$ ./gradlew build install -``` - -The quickest way to get started is to look at one of the two sample projects. Both -projects implement a RESTful service for creating tagged notes and illustrate the use -of Maven or Gradle. The two projects have different implementations: -`rest-notes-spring-hateoas` is implemented using Spring MVC and Spring Hateoas where as -`rest-notes-spring-data-rest` is implemented using Spring Data REST. - -Every example request and response in the documentation is auto-generated using custom -Spring MVC Test result handlers. This ensures that the examples match the service that -they are documenting. - -### Building a sample with Gradle - -To see the sample project's documentation, move into its directory and use Gradle to -build it. For example: - -``` -$ cd samples/rest-notes-spring-data-rest -$ ./gradlew asciidoctor -``` - -Once the build is complete, open one of the following: - -- build/asciidoc/html5/getting-started-guide.html -- build/asciidoc/html5/api-guide.html - -### Building a sample with Maven - -To see the sample project's documentation, move into its directory and use Maven to build -it. For example: - -``` -$ cd samples/rest-notes-spring-hateoas -$ mvn package -``` - -Once the build is complete, open one of the following: - -- target/generated-docs/getting-started-guide.html -- target/generated-docs/api-guide.html - -## How it works - -There are three main pieces involved in using this project to document your RESTful -service. +[slides][7] and a [video recording][8] are available. -### Build configuration - -Both Maven and Gradle are supported. - -#### Gradle configuration - -You can look at either samples' `build.gradle` file to see the required configuration. -The key parts are described below. - -Configure the Asciidoctor plugin: - -```groovy -plugins { - id "org.asciidoctor.convert" version "1.5.2" -} -``` - -Add a dependency on `spring-restdocs` in the `testCompile` configuration: - -```groovy -dependencies { - testCompile 'org.springframework.restdocs:spring-restdocs:0.1.0.BUILD-SNAPSHOT' -} -``` - -Configure a property to control the location of the generated snippets: - -```groovy -ext { - generatedDocumentation = file('build/generated-snippets') -} -``` - -Configure the `test` task with a system property to control the location to which the -snippets are generated: - -```groovy -test { - systemProperty 'org.springframework.restdocs.outputDir', generatedDocumentation - outputs.dir generatedDocumentation -} -``` - -Configure the `asciidoctor` task. The `generated` attribute is used to provide easy -access to the generated snippets: - -```groovy -asciidoctor { - attributes 'generated': generatedDocumentation - inputs.dir generatedDocumentation - dependsOn test -} - -``` - -You may want to include the generated documentation in your project's jar file, for -example to have it [served as static content by Spring Boot][12]. You can do so by -configuring the `jar` task to depend on the `asciidoctor` task and to copy the -generated documentation into the jar's `static` directory: - -```groovy -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -``` - -#### Maven configuration - -You can look at either samples' `pom.xml` file to see the required configuration. The key -parts are described below: - -Add a dependency on `spring-restdocs` in the `test` scope: - -```xml - - org.springframework.restdocs - spring-restdocs - 0.1.0.BUILD-SNAPSHOT - test - -``` - -Configure the SureFire plugin with a system property to control the location to which -the snippets are generated: - -```xml - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Documentation.java - - - ${project.build.directory}/generated-snippets - - - -``` - -Configure the Asciidoctor plugin. The `generated` attribute is used to provide easy -access to the generated snippets: - -```xml - - org.asciidoctor - asciidoctor-maven-plugin - 1.5.2 - - - generate-docs - package - - process-asciidoc - - - html - book - - ${project.build.directory}/generated-snippets - - - - - -``` - -You may want to include the generated documentation in your project's jar file, for -example to have it [served as static content by Spring Boot][12]. You can do so by -changing the configuration of the Asciidoctor plugin so that it runs in the -`prepare-package` phase and then configuring Maven's resources plugin to copy the -generated documentation into a location where it'll be included in the project's jar: - -```xml - - org.asciidoctor - asciidoctor-maven-plugin - 1.5.2 - - - generate-docs - prepare-package - - - - - - maven-resources-plugin - 2.7 - - - copy-resources - prepare-package - - copy-resources - - - ${project.build.outputDirectory}/static/docs - - - ${project.build.directory}/generated-docs - - - - - - -``` - -### Programatically generated snippets - -Spring's MVC Test framework is used to make requests to the service that you are -documenting. A custom `ResultHandler` is used to produce individual documentation -snippets for its request and its response as well as a snippet that contains both its -request and its response. - -The first step is to create a `MockMvc` instance using `MockMvcBuilders`, configuring -it by applying a `RestDocumentationConfigurer` that can be obtained from the static -`RestDocumentation.documentationConfiguration` method: - -```java -@Before -public void setUp() { - this.mockMvc = MockMvcBuilders - .webAppContextSetup(this.context) - .apply(documentationConfiguration()) - .build(); -} -``` - -This will apply the default REST documentation configuration: - -| Setting | Default value -| ---------------- | ------------- -| Scheme | http -| Host | localhost -| Port | 8080 -| Context path | Empty string -| Snippet encoding | UTF-8 - -One or more of these settings can be overridden: - -```java -@Before -public void setUp() { - this.mockMvc = MockMvcBuilders - .webAppContextSetup(this.context) - .apply(documentationConfiguration() - .uris() - .withScheme("https") - .withHost("api.example.com") - .withPort(443) - .withContextPath("/v3") - .and().snippets() - .withEncoding("ISO-8859-1")) - .build(); -} -``` - -To document a MockMvc call, you use MockMvc's `andDo` method, passing it a -`RestDocumentationResultHandler` that can be easily obtained from -the static `RestDocumentation.document` method: - -```java -public void getIndex() { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andDo(document("index")); -} -``` - -The code above will perform a `GET` request against the index (`/`) of the service with -an accept header indicating that a JSON response is required. It will write the cURL -command for the request and the resulting response to files in a directory named -`index` in the project's `build/generated-snippets/` directory. Three files will -be written: - - - `index/curl-request.adoc` - - `index/http-request.adoc` - - `index/http-response.adoc` - -#### Parameterized output directories - -The `document` method supports parameterized output directories. The following parameters -are supported: - -| Parameter | Description -| ------------- | ----------- -| {methodName} | The name of the test method, formatted using camelcase -| {method-name} | The name of the test method, formatted with dash separators -| {method_name} | The name of the test method, formatted with underscore separators -| {step} | The count of calls to `MockMvc.perform` in the current test - -For example, `document("{method-name}")` in a test method named `creatingANote` will -write snippets into a directory named `creating-a-note`. - -The `{step}` parameter is particularly useful in combination with Spring MVC Test's -`alwaysDo` functionality. It allows documentation to be configured once in a setup method: - -```java -@Before -public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()) - .alwaysDo(document("{method-name}/{step}/")) - .build(); -} -``` - -With this configuration in place, every call to `MockMvc.perform` will produce -documentation snippets without any further configuration. Take a look at the -`GettingStartedDocumentation` classes in each of the sample applications to see this -functionality in action. - -#### Pretty-printed snippets - -To improve the readability of the generated snippets you may want to configure your -application to produce JSON that has been pretty-printed. If you are a Spring Boot -user you can do so by setting the `spring.jackson.serialization.indent_output` property -to `true`. +## Learning more -### Hand-written documentation +To learn more about Spring REST Docs, please consult the [reference documentation][9]. -Producing high-quality, easily readable documentation is difficult and the process is -only made harder by trying to write the documentation in an ill-suited format such as -Java annotations. This project addresses this by allowing you to write the bulk of -your documentation's text using [Asciidoctor][1]. The default location for source files -depends on whether you're using Maven or Gradle. By default, Asciidoctor's Maven plugin -looks in `src/main/asciidoc`, whereas the Asciidoctor Gradle plugin looks in -`src/docs/asciidoc` +## Building from source -To include the programmatically generated snippets in your documentation, you use -Asciidoc's [`include` macro][6]. The Maven and Gradle configuration described above -configures an attribute, `generated`, that you can use to reference the directory to -which the snippets are written. For example, to include both the request and response -snippets described above: +Spring REST Docs requires Java 7 or later and is built using [Gradle][10]: ``` -include::{generated}/index/curl-request.adoc[] -include::{generated}/index/http-response.adoc[] +./gradlew build ``` -## Generating snippets in your IDE - -As described above, a system property is used to configure the location to which the -generated snippets are written. When running documentation tests in your IDE this system -property will not have been set. If the property is not set the snippets will be written -to standard out. +## Contributing -If you'd prefer that your IDE writes the snippets to disk you can use a file -in `src/test/resources` named `documentation.properties` to configure the property. -For example: - -```properties -org.springframework.restdocs.outputDir: target/generated-snippets - -``` - -## Learning more +[Pull requests][11] are welcome. Please see the [contributor guidelines][12] for details. -To learn more, take a look at the accompanying sample projects: +## Licence - - [rest-notes-spring-data-rest][7] - - [rest-notes-spring-hateoas][8] +Spring REST Docs is open source software released under the [Apache 2.0 license][13]. +[1]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) +[2]: https://build.spring.io/browse/SRD-PUB +[3]: http://asciidoctor.org +[4]: http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#spring-mvc-test-framework +[5]: https://developer.github.com/v3/ +[6]: http://swagger.io +[7]: https://speakerdeck.com/ankinson/documenting-restful-apis-webinar +[8]: https://www.youtube.com/watch?v=knH5ihPNiUs&feature=youtu.be +[9]: http://docs.spring.io/spring-restdocs/docs/ +[10]: http://gradle.org +[11]: https://help.github.com/articles/using-pull-requests/ +[12]: https://github.com/spring-projects/spring-restdocs/blob/master/CONTRIBUTING.md +[13]: http://www.apache.org/licenses/LICENSE-2.0.html -[1]: http://asciidoctor.org -[2]: http://docs.spring.io/spring-framework/docs/4.1.1.RELEASE/spring-framework-reference/html/testing.html#spring-mvc-test-framework -[3]: https://developer.github.com/v3/ -[4]: http://swagger.io -[5]: http://plugins.gradle.org/plugin/org.asciidoctor.gradle.asciidoctor -[6]: http://www.methods.co.nz/asciidoc/userguide.html#_system_macros -[7]: samples/rest-notes-spring-data-rest -[8]: samples/rest-notes-spring-hateoas -[9]: https://speakerdeck.com/ankinson/documenting-restful-apis-webinar -[10]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) -[11]: https://build.spring.io/browse/SRD-PUB -[12]: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-static-content -[13]: https://www.youtube.com/watch?v=knH5ihPNiUs&feature=youtu.be diff --git a/docs/build.gradle b/docs/build.gradle index da84abcb1..68c54bcbb 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -16,5 +16,6 @@ asciidoctor { sources { include 'index.adoc' } - attributes 'revnumber': project.version + attributes 'revnumber': project.version, + 'branch-or-tag': project.version.endsWith('SNAPSHOT') ? 'master': "v${project.version}" } \ No newline at end of file diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index fb92299b3..8824b28ab 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -29,7 +29,7 @@ change one or more of the defaults to suit your needs: [source,java,indent=0] ---- -include::{examples-dir}com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] +include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- @@ -43,7 +43,7 @@ use `RestDocumentationConfigurer` to configure it: [source,java,indent=0] ---- -include::{examples-dir}com/example/CustomEncoding.java[tags=custom-encoding] +include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] ---- diff --git a/docs/src/docs/asciidoc/contributing.adoc b/docs/src/docs/asciidoc/contributing.adoc new file mode 100644 index 000000000..04b7f4505 --- /dev/null +++ b/docs/src/docs/asciidoc/contributing.adoc @@ -0,0 +1,36 @@ +[[contributing]] +== Contributing + +Spring REST Docs is intended to make is easy for you to produce high-quality documentation +for your RESTful services. However, we can't achieve that goal without your contributions. + + + +[[contributing-questions]] +=== Questions + +You can ask questions about Spring REST Docs on http://stackoverflow.com[StackOverflow] +using the `spring-restdocs` tag. Similarly, we encourage you to help your fellow +Spring REST Docs users by answering questions. + + + +[[contributing-bugs]] +=== Bugs + +If you believe you have found a bug, please take a moment to search the +{github}/issues?is%3Aissue[existing issues]. If no one else has reported the problem, +please {github}/issues/new[open a new issue] that describes the problem in detail and, +ideally, includes a test that reproduces it. + + + +[[contributing-enhancements]] +=== Enhancements + +If you'd like an enhancement to be made to Spring REST Docs, pull requests are most +welcome. The source code is on {github}[GitHub]. You may want to search the +{github}/issues?is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] to +see if the enhancement is already being worked on. You may also want to +{github}/issues/new[open a new issue] to discuss a possible enhancement before work on it +begins. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8d58f1f43..4b00b430b 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -9,9 +9,12 @@ This section describes how to get started with Spring REST docs. === Sample applications If you want to jump straight in, there are two sample applications available. -{hateoas-sample}[One sample uses Spring HATEOAS] and {data-rest-sample}[the other uses -Spring Data REST]. Both samples use Spring REST Docs to produce a detailed API guide -and a getting started walkthrough. You can use either Gradle or Maven to build them. +{samples}/rest-notes-spring-hateoas[One sample] uses +http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and +{samples}/rest-notes-spring-data-rest[the other] uses +http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use +Spring REST Docs to produce a detailed API guide and a getting started walkthrough. You +can use either Gradle or Maven to build them. In each sample the source for the documentation can be found in `src/main/asciidoc`. `api-guide.adoc` produces an API guide for the service. `getting-started-guide.adoc` @@ -73,6 +76,26 @@ snippets that it generates. then use this attribute when including the generated snippets in your documentation. +[[getting-started-build-configuration-gradle-packaging-the-documentation]] +==== Packaging the documentation + +You may want to package the generated documentation in your project's jar file, for +example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as +static content] by Spring Boot. You can do so by configuring the `jar` task to depend on +the `asciidoctor` task and to copy the generated documentation into the jar's static +directory: + +[source,groovy,indent=0] +---- + jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } + } +---- + + [[getting-started-build-configuration-maven]] ==== Maven build configuration @@ -118,7 +141,7 @@ use as a reference. The key parts of the configuration are described below. generate-docs - package + package <5> process-asciidoc @@ -144,13 +167,66 @@ snippets that it generates. The plugin is also configured to include files whose with `Documentation.java`: <4> Configure the Asciidoctor plugin and define an attribute named `snippets`. You can then use this attribute when including the generated snippets in your documentation. +<5> [[getting-started-build-configuration-maven-plugin-phase]] If you want to +<> in your +project's jar you should use the `prepare-package` phase. + +[[getting-started-build-configuration-maven-packaging]] +==== Packaging the documentation + +You may want to package the generated documentation in your project's jar file, for +example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as +static content] by Spring Boot. + +First, configure the Asciidoctor plugin so that it runs in the `prepare-package` phase, as +<>. Now configure +Maven's resources plugin to copy the generated documentation into a location where it'll +be included in the project's jar: + +[source,xml,indent=0] +---- + <1> + org.asciidoctor + asciidoctor-maven-plugin + + + <2> + maven-resources-plugin + 2.7 + + + copy-resources + prepare-package + + copy-resources + + + + ${project.build.outputDirectory}/static/docs + + + + + ${project.build.directory}/generated-docs + + + + + + + +---- +<1> The existing declaration for the Asciidoctor plugin +<2> The resource plugin must be declared after the Asciidoctor plugin as they are bound +to the same phase and the resource plugin must run after the Asciidoctor plugin. + [[getting-started-documentation-snippets]] === Generating documentation snippets -Spring REST Docs uses {spring-mvc-test-docs}[Spring's MVC Test] to make requests to the -service that you are documenting. It then produces documentation snippets for the -result's request and response. +Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test +framework] to make requests to the service that you are documenting. It then produces +documentation snippets for the result's request and response. @@ -162,7 +238,7 @@ that creates a `MockMvc` instance: [source,java,indent=0] ---- -include::{examples-dir}com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] ---- The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An instance @@ -173,7 +249,7 @@ sensible defaults and also provides an API for customizing the configuration. Re -[[getting-started-documentation-snippets-setup]] +[[getting-started-documentation-snippets-invoking-the-service]] ==== Invoking the RESTful service Now that a `MockMvc` instance has been created, it can be used to invoke the RESTful @@ -181,7 +257,7 @@ service and document the request and response. [source,java,indent=0] ---- -include::{examples-dir}com/example/InvokeService.java[tags=invoke-service] +include::{examples-dir}/com/example/InvokeService.java[tags=invoke-service] ---- <1> Invoke the root (`/`) of the service an indicate that an `application/json` response is required diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 784fed0f4..99ec9d5f7 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -1,15 +1,17 @@ = Spring REST Docs Andy Wilkinson :doctype: book +:icons: font +:source-highlighter: highlightjs :toc: left :toclevels: 3 -:source-highlighter: highlightjs -:spring-mvc-test-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html#spring-mvc-test-framework -:samples: https://github.com/spring-projects/spring-restdocs/tree/master/samples -:hateoas-sample: https://github.com/spring-projects/spring-restdocs/tree/master/samples/rest-notes-spring-hateoas -:data-rest-sample: https://github.com/spring-projects/spring-restdocs/tree/master/samples/rest-notes-spring-data-rest -:examples-dir: ../../test/java/ -:icons: font +:sectlinks: + +:examples-dir: ../../test/java +:github: https://github.com/spring-projects/spring-restdocs +:samples: {github}/tree/{branch-or-tag}/samples +:spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle +:spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle [[abstract]] @@ -21,4 +23,5 @@ include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] include::configuration.adoc[] -include::working-with-asciidoctor.adoc[] \ No newline at end of file +include::working-with-asciidoctor.adoc[] +include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 8fceeb5ed..f85bedf18 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -10,9 +10,9 @@ http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produc HTML styled and layed out to suit your needs. Spring REST Docs makes use of snippets produced by tests written with -{spring-mvc-test-docs}[Spring MVC Test]. This test-driven approach helps to guarantee the -accuracy of your service's documentation. If a snippet is incorrect the test that -produces it will fail. +{spring-framework-docs}#spring-mvc-test-framework[Spring MVC Test]. This test-driven +approach helps to guarantee the accuracy of your service's documentation. If a snippet is +incorrect the test that produces it will fail. Documenting a RESTful service is largely about describing its resources. Two key parts of each resource's description are the details of the HTTP requests that it consumes diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 33824f456..c942ad233 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -41,9 +41,9 @@ included. [[working-with-asciidoctor-customizing-tables-formatting-columns]] ==== Formatting columns -Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format -[formatting a table's columns]. For example, the widths of a table's columns can be -specified using the `cols` attribute: +Asciidoctor has rich support for +http://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For +example, the widths of a table's columns can be specified using the `cols` attribute: [source,adoc,indent=0] ---- From f70565659522054491f90fa8b2820e8b21d0b655 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jun 2015 14:25:31 +0100 Subject: [PATCH 0084/1059] Polishing - Make inner classes static where possible - Declare classes as final where appropriate - Use try-with-resources - Improve readability by breaking up some large methods - Do not throw Throwable where possible --- .../restdocs/ResponseModifier.java | 6 ++- .../config/RestDocumentationContext.java | 2 +- .../restdocs/curl/CurlDocumentation.java | 44 +++++++++++++++---- .../hypermedia/LinkSnippetResultHandler.java | 23 ++++++---- .../restdocs/payload/FieldPath.java | 2 +- .../restdocs/payload/FieldProcessor.java | 8 ++-- .../ContentModifyingReponsePostProcessor.java | 4 +- .../HeaderRemovingResponsePostProcessor.java | 4 +- .../PrettyPrintingResponsePostProcessor.java | 8 ++-- .../snippet/DocumentationProperties.java | 23 +++------- .../snippet/SnippetWritingResultHandler.java | 6 +-- 11 files changed, 78 insertions(+), 52 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java index 40c04285e..f2415a302 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java @@ -16,6 +16,7 @@ package org.springframework.restdocs; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; @@ -36,7 +37,7 @@ * @see RestDocumentation#modifyResponseTo(ResponsePostProcessor...) * @author Andy Wilkinson */ -public class ResponseModifier { +public final class ResponseModifier { private final List postProcessors; @@ -98,7 +99,8 @@ private GetResponseMethodInterceptor(MockHttpServletResponse response, @Override public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws Throwable { + MethodProxy methodProxy) throws IllegalAccessException, + InvocationTargetException { if (this.getResponseMethod.equals(method)) { return this.response; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java index cbeeca1ed..2f48619df 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java @@ -25,7 +25,7 @@ * * @author Andy Wilkinson */ -public class RestDocumentationContext { +public final class RestDocumentationContext { private static final ThreadLocal CONTEXTS = new InheritableThreadLocal(); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 05199d440..a022fdcdf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -83,13 +83,37 @@ private static final class CurlRequestDocumentationAction implements public void perform() throws IOException { DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( this.result.getRequest()); - this.writer.print(String.format("curl '%s://%s", request.getScheme(), + + this.writer.print("curl '"); + + writeAuthority(request); + writePathAndQueryString(request); + + this.writer.print("'"); + + writeOptionToIncludeHeadersInOutput(); + writeHttpMethodIfNecessary(request); + writeHeaders(request); + writeContent(request); + + this.writer.println(); + } + + private void writeAuthority(DocumentableHttpServletRequest request) { + this.writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); if (isNonStandardPort(request)) { this.writer.print(String.format(":%d", request.getPort())); } + } + private boolean isNonStandardPort(DocumentableHttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); + } + + private void writePathAndQueryString(DocumentableHttpServletRequest request) { if (StringUtils.hasText(request.getContextPath())) { this.writer.print(String.format( request.getContextPath().startsWith("/") ? "%s" : "/%s", @@ -97,20 +121,29 @@ public void perform() throws IOException { } this.writer.print(request.getRequestUriWithQueryString()); + } - this.writer.print("' -i"); + private void writeOptionToIncludeHeadersInOutput() { + this.writer.print(" -i"); + } + private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request) { if (!request.isGetRequest()) { this.writer.print(String.format(" -X %s", request.getMethod())); } + } + private void writeHeaders(DocumentableHttpServletRequest request) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { this.writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } + } + private void writeContent(DocumentableHttpServletRequest request) + throws IOException { if (request.getContentLength() > 0) { this.writer .print(String.format(" -d '%s'", request.getContentAsString())); @@ -121,13 +154,6 @@ else if (request.isPostRequest() || request.isPutRequest()) { this.writer.print(String.format(" -d '%s'", queryString)); } } - - this.writer.println(); - } - - private boolean isNonStandardPort(DocumentableHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 033364cd5..4702930a1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -63,25 +63,29 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { @Override protected void handle(MvcResult result, DocumentationWriter writer) throws IOException { - Map> links; + validate(extractLinks(result)); + writeDocumentationSnippet(writer); + } + + private Map> extractLinks(MvcResult result) throws IOException { if (this.extractor != null) { - links = this.extractor.extractLinks(result.getResponse()); + return this.extractor.extractLinks(result.getResponse()); } else { String contentType = result.getResponse().getContentType(); LinkExtractor extractorForContentType = LinkExtractors .extractorForContentType(contentType); if (extractorForContentType != null) { - links = extractorForContentType.extractLinks(result.getResponse()); - } - else { - throw new IllegalStateException( - "No LinkExtractor has been provided and one is not available for the content type " - + contentType); + return extractorForContentType.extractLinks(result.getResponse()); } + throw new IllegalStateException( + "No LinkExtractor has been provided and one is not available for the content type " + + contentType); } + } + private void validate(Map> links) { Set actualRels = links.keySet(); Set undocumentedRels = new HashSet(actualRels); @@ -105,7 +109,9 @@ protected void handle(MvcResult result, DocumentationWriter writer) } throw new SnippetGenerationException(message); } + } + private void writeDocumentationSnippet(DocumentationWriter writer) throws IOException { writer.table(new TableAction() { @Override @@ -118,7 +124,6 @@ public void perform(TableWriter tableWriter) throws IOException { } }); - } } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java index cbb87a12a..a1dfb0339 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java @@ -27,7 +27,7 @@ * @author Andy Wilkinson * */ -class FieldPath { +final class FieldPath { private static final Pattern ARRAY_INDEX_PATTERN = Pattern .compile("\\[([0-9]+|\\*){0,1}\\]"); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java index 0407e567e..bfa7f20a8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -29,7 +29,7 @@ * @author Andy Wilkinson * */ -class FieldProcessor { +final class FieldProcessor { boolean hasField(FieldPath fieldPath, Object payload) { final AtomicReference hasField = new AtomicReference(false); @@ -122,7 +122,7 @@ private void handleMapPayload(ProcessingContext context, MatchCallback matchCall } } - private final class MapMatch implements Match { + private static final class MapMatch implements Match { private final Object item; @@ -154,7 +154,7 @@ public void remove() { } - private final class ListMatch implements Match { + private static final class ListMatch implements Match { private final Iterator items; @@ -199,7 +199,7 @@ private interface Match { void remove(); } - private static class ProcessingContext { + private static final class ProcessingContext { private final Object payload; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java index 67bc6ddce..977eaf5c6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.response; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.springframework.cglib.proxy.Enhancer; @@ -74,7 +75,8 @@ public ContentModifyingMethodInterceptor(String modifiedContent, @Override public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws Throwable { + MethodProxy methodProxy) throws IllegalAccessException, + InvocationTargetException { if (this.getContentAsStringMethod.equals(method)) { return this.modifiedContent; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java index e3b926d0b..c7cc69a10 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.response; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -78,7 +79,8 @@ private HeaderHidingMethodInterceptor(Set hiddenHeaders, @Override public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws Throwable { + MethodProxy methodProxy) throws IllegalAccessException, + InvocationTargetException { if (this.getHeaderNamesMethod.equals(method)) { List headerNames = new ArrayList<>(); for (String candidate : this.response.getHeaderNames()) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java index 41bd7f5bf..4e457c803 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java @@ -20,6 +20,7 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.Arrays; +import java.util.Collections; import java.util.List; import javax.xml.transform.OutputKeys; @@ -35,13 +36,14 @@ class PrettyPrintingResponsePostProcessor extends ContentModifyingReponsePostProcessor { - private static final List prettyPrinters = Arrays.asList( - new JsonPrettyPrinter(), new XmlPrettyPrinter()); + private static final List PRETTY_PRINTERS = Collections + .unmodifiableList(Arrays.asList(new JsonPrettyPrinter(), + new XmlPrettyPrinter())); @Override protected String modifyContent(String originalContent) { if (StringUtils.hasText(originalContent)) { - for (PrettyPrinter prettyPrinter : prettyPrinters) { + for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { try { return prettyPrinter.prettyPrint(originalContent); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java index f8ca87b12..3241ce04f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java @@ -28,25 +28,16 @@ class DocumentationProperties { private final Properties properties = new Properties(); DocumentationProperties() { - InputStream stream = getClass().getClassLoader().getResourceAsStream( - "documentation.properties"); - if (stream != null) { - try { + try (InputStream stream = getClass().getClassLoader().getResourceAsStream( + "documentation.properties")) { + if (stream != null) { this.properties.load(stream); } - catch (IOException ex) { - throw new IllegalStateException( - "Failed to read documentation.properties", ex); - } - finally { - try { - stream.close(); - } - catch (IOException e) { - // Continue - } - } } + catch (IOException ex) { + throw new IllegalStateException("Failed to read documentation.properties", ex); + } + this.properties.putAll(System.getProperties()); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index d77635e6a..1bd057c9e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -48,13 +48,9 @@ protected abstract void handle(MvcResult result, DocumentationWriter writer) @Override public void handle(MvcResult result) throws IOException { - Writer writer = createWriter(); - try { + try (Writer writer = createWriter()) { handle(result, new AsciidoctorWriter(writer)); } - finally { - writer.close(); - } } private Writer createWriter() throws IOException { From cdcddcf91b072307dc9a939ca52c3ea7ef53e2a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jun 2015 14:36:47 +0100 Subject: [PATCH 0085/1059] Update the samples to use Spring Boot 1.2.4.RELEASE --- samples/rest-notes-spring-data-rest/build.gradle | 3 +-- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/rest-notes-spring-hateoas/pom.xml | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle index bd0b39c31..93d03fb04 100644 --- a/samples/rest-notes-spring-data-rest/build.gradle +++ b/samples/rest-notes-spring-data-rest/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE' } } @@ -35,7 +35,6 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile 'org.springframework.restdocs:spring-restdocs:1.0.0.BUILD-SNAPSHOT' - } ext { diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index e8a61e183..26e4f7e31 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.1.RELEASE + 1.2.4.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 249c02481..d7c9a4c31 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE' } } diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml index 350d0a72c..b46f0db1c 100644 --- a/samples/rest-notes-spring-hateoas/pom.xml +++ b/samples/rest-notes-spring-hateoas/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.1.RELEASE + 1.2.4.RELEASE From 88e26ba88b88e8e064772ec14a6b7f8778185d81 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Jul 2015 11:44:06 +0100 Subject: [PATCH 0086/1059] Improve diagnostics when no type is provided for absent optional field Previously, if an optional field was being documented and that field was not present in the payload a failure would occur with the message "The payload does not contain a field with the path 'the.field.path'". This isn't very helpful as it doesn't explain why the field was being looked for (to resolve its type). This commit improves the diagnostics to improve the message to explain that a field's type could not be determined as it didn't exist in the payload and to suggest the use of FieldDescriptor.type(FieldType) to provide a type. Closes gh-83 --- .../payload/FieldDoesNotExistException.java | 37 +++++++++++++++++++ .../restdocs/payload/FieldProcessor.java | 3 +- .../payload/FieldSnippetResultHandler.java | 23 ++++++++++-- .../payload/FieldTypeRequiredException.java | 37 +++++++++++++++++++ .../restdocs/payload/FieldProcessorTests.java | 14 +++---- .../payload/FieldTypeResolverTests.java | 2 +- .../payload/PayloadDocumentationTests.java | 8 ++++ 7 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java new file mode 100644 index 000000000..99f565e23 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * A {@code FieldDoesNotExistException} is thrown when a requested field does not exist in + * a payload. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("serial") +public class FieldDoesNotExistException extends RuntimeException { + + /** + * Creates a new {@code FieldDoesNotExistException} that indicates that the field with + * the given {@code fieldPath} does not exist. + * + * @param fieldPath the path of the field that does not exist + */ + public FieldDoesNotExistException(FieldPath fieldPath) { + super("The payload does not contain a field with the path '" + fieldPath + "'"); + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java index bfa7f20a8..f0e3d25d2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java @@ -55,8 +55,7 @@ public void foundMatch(Match match) { }); if (matches.isEmpty()) { - throw new IllegalArgumentException( - "The payload does not contain a field with the path '" + path + "'"); + throw new FieldDoesNotExistException(path); } if (path.isPrecise()) { return matches.get(0); 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 index 5ad941578..84627c420 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -78,15 +78,32 @@ public void perform(TableWriter tableWriter) throws IOException { 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); + FieldType type = getFieldType(descriptor, payload); tableWriter.row(entry.getKey().toString(), type.toString(), entry .getValue().getDescription()); } } + private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { + if (descriptor.getType() != null) { + return descriptor.getType(); + } + else { + try { + return FieldSnippetResultHandler.this.fieldTypeResolver + .resolveFieldType(descriptor.getPath(), payload); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the" + + " payload. Please provide a type using" + + " FieldDescriptor.type(FieldType)."; + throw new FieldTypeRequiredException(message); + } + } + } + }); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java new file mode 100644 index 000000000..c9fa41215 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * A {@code FieldTypeRequiredException} is thrown when a field's type cannot be determined + * automatically and, therefore, must be explicitly provided. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("serial") +public class FieldTypeRequiredException extends RuntimeException { + + /** + * Creates a new {@code FieldTypeRequiredException} indicating that a type is required + * for the reason described in the given {@code message}. + * + * @param message the message + */ + public FieldTypeRequiredException(String message) { + super(message); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java index 949ae10fa..da71af5e0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java @@ -129,27 +129,27 @@ public void extractArraysFromItemsInNestedArray() { Arrays.asList(4)))); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentTopLevelField() { this.fieldProcessor .extract(FieldPath.compile("a"), new HashMap()); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedField() { HashMap payload = new HashMap(); payload.put("a", new HashMap()); this.fieldProcessor.extract(FieldPath.compile("a.b"), payload); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedFieldWhenParentIsNotAMap() { HashMap payload = new HashMap(); payload.put("a", 5); this.fieldProcessor.extract(FieldPath.compile("a.b"), payload); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldWhenParentIsAnArray() { HashMap payload = new HashMap(); HashMap alpha = new HashMap(); @@ -158,20 +158,20 @@ public void nonExistentFieldWhenParentIsAnArray() { this.fieldProcessor.extract(FieldPath.compile("a.b.c"), payload); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayField() { HashMap payload = new HashMap(); this.fieldProcessor.extract(FieldPath.compile("a[]"), payload); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayFieldAsTypeDoesNotMatch() { HashMap payload = new HashMap(); payload.put("a", 5); this.fieldProcessor.extract(FieldPath.compile("a[]"), payload); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldBeneathAnArray() { HashMap payload = new HashMap(); HashMap alpha = new HashMap(); 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 index 42d5fc9f8..4376bea55 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -93,7 +93,7 @@ public void multipleFieldsWithDifferentTypes() throws IOException { @Test public void nonExistentFieldProducesIllegalArgumentException() throws IOException { - this.thrownException.expect(IllegalArgumentException.class); + this.thrownException.expect(FieldDoesNotExistException.class); this.thrownException .expectMessage("The payload does not contain a field with the path 'a.b'"); this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}")); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index e8750ccfa..7e99a1457 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -141,6 +141,14 @@ public void missingRequestField() throws IOException { result(get("/foo").content("{}"))); } + @Test + public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + documentRequestFields("missing-optional-request-field-with-no-type", + fieldWithPath("a.b").description("one").optional()).handle( + result(get("/foo").content("{ }"))); + } + @Test public void undocumentedRequestFieldAndMissingRequestField() throws IOException { this.thrown.expect(SnippetGenerationException.class); From 5d0428b9d0c3ae0f8345ae4dfcf92e691952d547 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Jul 2015 17:32:38 +0100 Subject: [PATCH 0087/1059] Add support for documenting multipart requests This commit adds support for documenting multipart requests in both curl and HTTP request snippets. Closes gh-48 --- .../restdocs/curl/CurlDocumentation.java | 28 +++++++++++ .../restdocs/http/HttpDocumentation.java | 48 ++++++++++++++++++- .../util/DocumentableHttpServletRequest.java | 28 +++++++++++ .../restdocs/curl/CurlDocumentationTests.java | 46 ++++++++++++++++++ .../restdocs/http/HttpDocumentationTests.java | 33 +++++++++++++ 5 files changed, 182 insertions(+), 1 deletion(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a022fdcdf..45e4d0817 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; /** * Static factory methods for documenting a RESTful API as if it were being driven using @@ -94,6 +95,11 @@ public void perform() throws IOException { writeOptionToIncludeHeadersInOutput(); writeHttpMethodIfNecessary(request); writeHeaders(request); + + if (request.isMultipartRequest()) { + writeParts(request); + } + writeContent(request); this.writer.println(); @@ -142,6 +148,28 @@ private void writeHeaders(DocumentableHttpServletRequest request) { } } + private void writeParts(DocumentableHttpServletRequest request) + throws IOException { + for (Entry> entry : request.getMultipartFiles() + .entrySet()) { + for (MultipartFile file : entry.getValue()) { + this.writer.printf(" -F '%s=", file.getName()); + if (!StringUtils.hasText(file.getOriginalFilename())) { + this.writer.append(new String(file.getBytes())); + } + else { + this.writer.printf("@%s", file.getOriginalFilename()); + } + + if (StringUtils.hasText(file.getContentType())) { + this.writer.append(";type=").append(file.getContentType()); + } + this.writer.append("'"); + } + } + + } + private void writeContent(DocumentableHttpServletRequest request) throws IOException { if (request.getContentLength() > 0) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 0e1b9fc9f..5c569f1c1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -29,6 +29,7 @@ import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; /** * Static factory methods for documenting a RESTful API's HTTP requests. @@ -82,6 +83,8 @@ public void handle(MvcResult result, DocumentationWriter writer) private static class HttpRequestDocumentationAction implements DocumentationAction { + private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + private final DocumentationWriter writer; private final MvcResult result; @@ -97,9 +100,18 @@ public void perform() throws IOException { this.result.getRequest()); this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), request.getRequestUriWithQueryString()); + for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { - this.writer.printf("%s: %s%n", header.getKey(), value); + if (header.getKey() == HttpHeaders.CONTENT_TYPE + && request.isMultipartRequest()) { + this.writer.printf("%s: %s; boundary=%s%n", header.getKey(), + value, MULTIPART_BOUNDARY); + } + else { + this.writer.printf("%s: %s%n", header.getKey(), value); + } + } } if (requiresFormEncodingContentType(request)) { @@ -115,6 +127,9 @@ else if (request.isPostRequest() || request.isPutRequest()) { if (StringUtils.hasText(queryString)) { this.writer.println(queryString); } + if (request.isMultipartRequest()) { + writeParts(request); + } } } @@ -124,6 +139,37 @@ private boolean requiresFormEncodingContentType( && (request.isPostRequest() || request.isPutRequest()) && StringUtils.hasText(request.getParameterMapAsQueryString()); } + + private void writeParts(DocumentableHttpServletRequest request) + throws IOException { + for (Entry> entry : request.getMultipartFiles() + .entrySet()) { + for (MultipartFile file : entry.getValue()) { + writePartBoundary(); + writePart(file); + this.writer.println(); + } + } + writeMultipartEnd(); + } + + private void writePartBoundary() { + this.writer.printf("--%s%n", MULTIPART_BOUNDARY); + } + + private void writePart(MultipartFile part) throws IOException { + this.writer.printf("Content-Disposition: form-data; name=%s%n", + part.getName()); + if (StringUtils.hasText(part.getContentType())) { + this.writer.printf("Content-Type: %s%n", part.getContentType()); + } + this.writer.println(); + this.writer.print(new String(part.getBytes())); + } + + private void writeMultipartEnd() { + this.writer.printf("--%s--%n", MULTIPART_BOUNDARY); + } } private static final class HttpResponseDocumentationAction implements diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java index a4779722d..4f7e6e069 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -28,9 +28,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.util.FileCopyUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; /** * An {@link HttpServletRequest} wrapper that provides a limited set of methods intended @@ -83,6 +86,31 @@ public boolean isPutRequest() { return RequestMethod.PUT == RequestMethod.valueOf(this.delegate.getMethod()); } + /** + * Whether or not this is a multipart request. + * + * @return {@code true} if it is a multipart request, otherwise {@code false}. + * @see MockMultipartHttpServletRequest + */ + public boolean isMultipartRequest() { + return this.delegate instanceof MockMultipartHttpServletRequest; + } + + /** + * Returns a {@code Map} of the request's multipart files, or {@code null} if this + * request is not a multipart request. + * + * @return a {@code Map} of the multipart files contained in the request, or + * {@code null} + * @see #isMultipartRequest() + */ + public MultiValueMap getMultipartFiles() { + if (!isMultipartRequest()) { + return null; + } + return ((MockMultipartHttpServletRequest) this.delegate).getMultiFileMap(); + } + /** * Returns the request's headers. The headers are ordered based on the ordering of * {@link HttpServletRequest#getHeaderNames()} and diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index dfda7b681..faa25a0f8 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -19,6 +19,7 @@ import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @@ -27,8 +28,10 @@ import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -44,6 +47,9 @@ public class CurlDocumentationTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( @@ -241,4 +247,44 @@ public void requestWithContextPathWithoutSlash() throws IOException { result(request)); } + @Test + public void multipartPostWithNoOriginalFilename() throws IOException { + String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + + "'Content-Type: multipart/form-data' -F " + + "'metadata={\"description\": \"foo\"}'"; + this.snippet.expectCurlRequest("multipart-post-no-original-filename") + .withContents(codeBlock("bash").content(expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("metadata", + "{\"description\": \"foo\"}".getBytes()); + documentCurlRequest("multipart-post-no-original-filename").handle( + result(fileUpload("/upload").file(multipartFile))); + } + + @Test + public void multipartPostWithContentType() throws IOException { + String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + + "'Content-Type: multipart/form-data' -F " + + "'image=@documents/images/example.png;type=image/png'"; + this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( + codeBlock("bash").content(expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, + "bytes".getBytes()); + documentCurlRequest("multipart-post-with-content-type").handle( + result(fileUpload("/upload").file(multipartFile))); + } + + @Test + public void multipartPost() throws IOException { + String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + + "'Content-Type: multipart/form-data' -F " + + "'image=@documents/images/example.png'"; + this.snippet.expectCurlRequest("multipart-post").withContents( + codeBlock("bash").content(expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", null, "bytes".getBytes()); + documentCurlRequest("multipart-post").handle( + result(fileUpload("/upload").file(multipartFile))); + } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index a1e305627..73a639e13 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -23,6 +23,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @@ -36,6 +37,7 @@ import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -156,4 +158,35 @@ public void responseWithContent() throws IOException { documentHttpResponse("response-with-content").handle(result(response)); } + @Test + public void multipartPost() throws IOException { + String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + String expectedContent = String.format("--%s%nContent-Disposition: form-data; " + + "name=image%n%n<< data >>%n--%s--", boundary, boundary); + this.snippet.expectHttpRequest("multipart-post").withContents( + httpRequest(POST, "/upload").header("Content-Type", + "multipart/form-data; boundary=" + boundary).content( + expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", null, "<< data >>".getBytes()); + documentHttpRequest("multipart-post").handle( + result(fileUpload("/upload").file(multipartFile))); + } + + @Test + public void multipartPostWithContentType() throws IOException { + String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + String expectedContent = String.format("--%s%nContent-Disposition: form-data; " + + "name=image%nContent-Type: image/png%n%n<< data >>%n--%s--", boundary, + boundary); + this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( + httpRequest(POST, "/upload").header("Content-Type", + "multipart/form-data; boundary=" + boundary).content( + expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, + "<< data >>".getBytes()); + documentHttpRequest("multipart-post-with-content-type").handle( + result(fileUpload("/upload").file(multipartFile))); + } } From 5d44015e7d26866a503d8f806aafdf24205552df Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Jul 2015 17:46:21 +0100 Subject: [PATCH 0088/1059] Improve description of when fields are considered to be documented Closes gh-82 --- .../docs/asciidoc/documenting-your-api.adoc | 7 ++----- .../RestDocumentationResultHandler.java | 18 +++++++++++------- .../restdocs/payload/PayloadDocumentation.java | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c8db048af..d2ac40571 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -77,11 +77,8 @@ snippet is named `request-fields.adoc`. For responses this snippet is named When documenting fields, the test will fail if an undocumented field is found in the payload. Similarly, the test will also fail if a documented field is not found in the payload and the field has not be marked as optional. For payloads with a hierarchical -structure, when a point in the hierarchy is documented any fields beneath that point are -also deemed to have been documented. This means that you do not have to document the -entire hierarchy, but you may do so if you wish. - - +structure, documenting a field is sufficient for all of its descendants to also be +treated as having been documented. [[documenting-your-api-request-response-payloads-field-paths]] ==== Field paths 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 4beea6a78..203d3152c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -101,10 +101,12 @@ public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, /** * Document the fields in the request using the given {@code descriptors}. *

- * 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. + * If a field is present in the request but is not documented by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request a + * failure will also occur. For payloads with a hierarchical structure, documenting a + * field is sufficient for all of its descendants to also be treated as having been + * documented. * * @param descriptors the link descriptors * @return {@code this} @@ -119,10 +121,12 @@ public RestDocumentationResultHandler withRequestFields( /** * 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 + * If a field is present in the response but is not documented 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. + * field is documented, is not marked as optional, and is not present in the response + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. * * @param descriptors the link descriptors * @return {@code this} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 217825cad..2b39a1b25 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -98,11 +98,19 @@ public static FieldDescriptor fieldWithPath(String path) { /** * Creates a {@code RequestFieldsSnippetResultHandler} that will produce a * documentation snippet for a request's fields. + *

+ * If a field is present in the request but is not documented by one of the + * descriptors a failure will occur when the handler is invoked. Similarly, if a field + * is documented, is not marked as optional, and is not present in the request a + * failure will also occur. For payloads with a hierarchical structure, documenting a + * field is sufficient for all of its descendants to also be treated as having been + * documented. * * @param outputDir The directory to which the snippet should be written * @param descriptors The descriptions of the request's fields * @return the handler * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) + * @see #fieldWithPath(String) */ public static FieldSnippetResultHandler documentRequestFields(String outputDir, FieldDescriptor... descriptors) { @@ -112,6 +120,13 @@ public static FieldSnippetResultHandler documentRequestFields(String outputDir, /** * Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a * documentation snippet for a response's fields. + *

+ * If a field is present in the response but is not documented by one of the + * descriptors a failure will occur when the handler is invoked. Similarly, if a field + * is documented, is not marked as optional, and is not present in the response a + * failure will also occur. For payloads with a hierarchical structure, documenting a + * field is sufficient for all of its descendants to also be treated as having been + * documented. * * @param outputDir The directory to which the snippet should be written * @param descriptors The descriptions of the response's fields From 833a01d6a5de8b5b343de23697df04df0099057a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Jul 2015 18:05:39 +0100 Subject: [PATCH 0089/1059] Add a Host: header to HTTP request snippets HTTP 1.1 requires a Host: header in all requests. This commit updates the HTTP request snippet to ensure that such a header is always present. Closes gh-85 --- .../restdocs/http/HttpDocumentation.java | 18 ++++-- .../restdocs/http/HttpDocumentationTests.java | 61 +++++++++++++------ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 5c569f1c1..781024bd8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -100,13 +100,15 @@ public void perform() throws IOException { this.result.getRequest()); this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), request.getRequestUriWithQueryString()); - + if (requiresHostHeader(request)) { + writeHeader(HttpHeaders.HOST, request.getHost()); + } for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { if (header.getKey() == HttpHeaders.CONTENT_TYPE && request.isMultipartRequest()) { - this.writer.printf("%s: %s; boundary=%s%n", header.getKey(), - value, MULTIPART_BOUNDARY); + writeHeader(header.getKey(), String.format("%s; boundary=%s", + value, MULTIPART_BOUNDARY)); } else { this.writer.printf("%s: %s%n", header.getKey(), value); @@ -115,7 +117,7 @@ public void perform() throws IOException { } } if (requiresFormEncodingContentType(request)) { - this.writer.printf("%s: %s%n", HttpHeaders.CONTENT_TYPE, + writeHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); } this.writer.println(); @@ -133,6 +135,10 @@ else if (request.isPostRequest() || request.isPutRequest()) { } } + private boolean requiresHostHeader(DocumentableHttpServletRequest request) { + return request.getHeaders().get(HttpHeaders.HOST) == null; + } + private boolean requiresFormEncodingContentType( DocumentableHttpServletRequest request) { return request.getHeaders().getContentType() == null @@ -140,6 +146,10 @@ private boolean requiresFormEncodingContentType( && StringUtils.hasText(request.getParameterMapAsQueryString()); } + private void writeHeader(String name, String value) { + this.writer.printf("%s: %s%n", name, value); + } + private void writeParts(DocumentableHttpServletRequest request) throws IOException { for (Entry> entry : request.getMultipartFiles() diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index 73a639e13..b55ddec8c 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -35,7 +35,9 @@ import org.junit.Rule; import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.test.ExpectedSnippet; @@ -54,7 +56,8 @@ public class HttpDocumentationTests { @Test public void getRequest() throws IOException { this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(GET, "/foo").header("Alpha", "a")); + httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( + "Alpha", "a")); documentHttpRequest("get-request").handle( result(get("/foo").header("Alpha", "a"))); @@ -63,7 +66,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string").withContents( - httpRequest(GET, "/foo?bar=baz")); + httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); documentHttpRequest("get-request-with-query-string").handle( result(get("/foo?bar=baz"))); @@ -72,7 +75,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("get-request-with-parameter").withContents( - httpRequest(GET, "/foo?b%26r=baz")); + httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost")); documentHttpRequest("get-request-with-parameter").handle( result(get("/foo").param("b&r", "baz"))); @@ -81,8 +84,8 @@ public void getRequestWithParameter() throws IOException { @Test public void postRequestWithContent() throws IOException { this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(POST, "/foo") // - .content("Hello, world")); + httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( + "Hello, world")); documentHttpRequest("post-request-with-content").handle( result(post("/foo").content("Hello, world"))); @@ -91,8 +94,8 @@ public void postRequestWithContent() throws IOException { @Test public void postRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("post-request-with-parameter").withContents( - httpRequest(POST, "/foo") // - .header("Content-Type", "application/x-www-form-urlencoded") // + httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); documentHttpRequest("post-request-with-parameter").handle( @@ -102,8 +105,8 @@ public void postRequestWithParameter() throws IOException { @Test public void putRequestWithContent() throws IOException { this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(PUT, "/foo") // - .content("Hello, world")); + httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( + "Hello, world")); documentHttpRequest("put-request-with-content").handle( result(put("/foo").content("Hello, world"))); @@ -112,8 +115,8 @@ public void putRequestWithContent() throws IOException { @Test public void putRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("put-request-with-parameter").withContents( - httpRequest(PUT, "/foo") // - .header("Content-Type", "application/x-www-form-urlencoded") // + httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); documentHttpRequest("put-request-with-parameter").handle( @@ -164,9 +167,11 @@ public void multipartPost() throws IOException { String expectedContent = String.format("--%s%nContent-Disposition: form-data; " + "name=image%n%n<< data >>%n--%s--", boundary, boundary); this.snippet.expectHttpRequest("multipart-post").withContents( - httpRequest(POST, "/upload").header("Content-Type", - "multipart/form-data; boundary=" + boundary).content( - expectedContent)); + httpRequest(POST, "/upload") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", + "multipart/form-data; boundary=" + boundary) + .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); documentHttpRequest("multipart-post").handle( @@ -180,13 +185,35 @@ public void multipartPostWithContentType() throws IOException { + "name=image%nContent-Type: image/png%n%n<< data >>%n--%s--", boundary, boundary); this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( - httpRequest(POST, "/upload").header("Content-Type", - "multipart/form-data; boundary=" + boundary).content( - expectedContent)); + httpRequest(POST, "/upload") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", + "multipart/form-data; boundary=" + boundary) + .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "<< data >>".getBytes()); documentHttpRequest("multipart-post-with-content-type").handle( result(fileUpload("/upload").file(multipartFile))); } + + @Test + public void getRequestWithCustomServerName() throws IOException { + this.snippet.expectHttpRequest("get-request-custom-server-name").withContents( + httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setServerName("api.example.com"); + + documentHttpRequest("get-request-custom-server-name").handle(result(request)); + } + + @Test + public void getRequestWithCustomHost() throws IOException { + this.snippet.expectHttpRequest("get-request-custom-host").withContents( + httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); + + documentHttpRequest("get-request-custom-host").handle( + result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); + } } From 4c31dc0216e9e675c88692ff0f1d895efd0d6a5e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Jul 2015 16:03:26 +0100 Subject: [PATCH 0090/1059] Fix request parameters in curl snippet for multipart requests Previously, request parameters would be provided via -d. This is incorrect for a multipart request that is also using -F. This commit updates the generation of the curl request snippet to use -F for request parameters when the request is a multipart request. Closes gh-93 --- .../restdocs/curl/CurlDocumentation.java | 8 ++++++++ .../util/DocumentableHttpServletRequest.java | 9 +++++++++ .../restdocs/curl/CurlDocumentationTests.java | 15 +++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 45e4d0817..65368dc6d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -176,6 +176,14 @@ private void writeContent(DocumentableHttpServletRequest request) this.writer .print(String.format(" -d '%s'", request.getContentAsString())); } + else if (request.isMultipartRequest()) { + for (Entry entry : request.getParameterMap().entrySet()) { + for (String value : entry.getValue()) { + this.writer.print(String.format(" -F '%s=%s'", entry.getKey(), + value)); + } + } + } else if (request.isPostRequest() || request.isPutRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java index 4f7e6e069..0aff84b70 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -230,6 +230,15 @@ public String getContextPath() { return this.delegate.getContextPath(); } + /** + * Returns a map of the request's parameters + * @return The map of parameters + * @see HttpServletRequest#getParameterMap() + */ + public Map getParameterMap() { + return this.delegate.getParameterMap(); + } + private String getQueryString() { if (this.delegate.getQueryString() != null) { return this.delegate.getQueryString(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index faa25a0f8..3a359ef21 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -287,4 +287,19 @@ public void multipartPost() throws IOException { result(fileUpload("/upload").file(multipartFile))); } + @Test + public void multipartPostWithParameters() throws IOException { + String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + + "'Content-Type: multipart/form-data' -F " + + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + + "-F 'b=banana'"; + this.snippet.expectCurlRequest("multipart-post").withContents( + codeBlock("bash").content(expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", null, "bytes".getBytes()); + documentCurlRequest("multipart-post").handle( + result(fileUpload("/upload").file(multipartFile) + .param("a", "apple", "avocado").param("b", "banana"))); + } + } From f481e2fc8a3b9a095e179fe4847f26f356cac9d9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Jul 2015 16:29:31 +0100 Subject: [PATCH 0091/1059] Fix request parameter handling in multipart http request snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, request parameters for a POST were form encoded into the body, irrespective of the request’s type. This was incorrect for a multipart request where each request parameter should be included in a separate request part. This commit updates HttpDocumentation to treat request parameters differently when dealing with a multipart request. In such cases each request parameter is now included in the body of the request in a separate request part. Fixes gh-94 --- .../restdocs/http/HttpDocumentation.java | 32 +++++++---- .../restdocs/http/HttpDocumentationTests.java | 56 ++++++++++++++++--- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 781024bd8..025f17ed3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -125,13 +125,15 @@ public void perform() throws IOException { this.writer.println(request.getContentAsString()); } else if (request.isPostRequest() || request.isPutRequest()) { - String queryString = request.getParameterMapAsQueryString(); - if (StringUtils.hasText(queryString)) { - this.writer.println(queryString); - } if (request.isMultipartRequest()) { writeParts(request); } + else { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + this.writer.println(queryString); + } + } } } @@ -152,6 +154,13 @@ private void writeHeader(String name, String value) { private void writeParts(DocumentableHttpServletRequest request) throws IOException { + for (Entry parameter : request.getParameterMap().entrySet()) { + for (String value : parameter.getValue()) { + writePartBoundary(); + writePart(parameter.getKey(), value, null); + this.writer.println(); + } + } for (Entry> entry : request.getMultipartFiles() .entrySet()) { for (MultipartFile file : entry.getValue()) { @@ -167,14 +176,17 @@ private void writePartBoundary() { this.writer.printf("--%s%n", MULTIPART_BOUNDARY); } - private void writePart(MultipartFile part) throws IOException { - this.writer.printf("Content-Disposition: form-data; name=%s%n", - part.getName()); - if (StringUtils.hasText(part.getContentType())) { - this.writer.printf("Content-Type: %s%n", part.getContentType()); + private void writePart(String name, String value, String contentType) { + this.writer.printf("Content-Disposition: form-data; name=%s%n", name); + if (StringUtils.hasText(contentType)) { + this.writer.printf("Content-Type: %s%n", contentType); } this.writer.println(); - this.writer.print(new String(part.getBytes())); + this.writer.print(value); + } + + private void writePart(MultipartFile part) throws IOException { + writePart(part.getName(), new String(part.getBytes()), part.getContentType()); } private void writeMultipartEnd() { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index b55ddec8c..b2486e325 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -50,6 +50,8 @@ */ public class HttpDocumentationTests { + private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + @Rule public final ExpectedSnippet snippet = new ExpectedSnippet(); @@ -163,14 +165,13 @@ public void responseWithContent() throws IOException { @Test public void multipartPost() throws IOException { - String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - String expectedContent = String.format("--%s%nContent-Disposition: form-data; " - + "name=image%n%n<< data >>%n--%s--", boundary, boundary); + String expectedContent = createPart(String.format("Content-Disposition: " + + "form-data; " + "name=image%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post").withContents( httpRequest(POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", - "multipart/form-data; boundary=" + boundary) + "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); @@ -178,17 +179,40 @@ public void multipartPost() throws IOException { result(fileUpload("/upload").file(multipartFile))); } + @Test + public void multipartPostWithParameters() throws IOException { + String param1Part = createPart(String.format("Content-Disposition: form-data; " + + "name=a%n%napple"), false); + String param2Part = createPart(String.format("Content-Disposition: form-data; " + + "name=a%n%navocado"), false); + String param3Part = createPart(String.format("Content-Disposition: form-data; " + + "name=b%n%nbanana"), false); + String filePart = createPart(String.format("Content-Disposition: form-data; " + + "name=image%n%n<< data >>")); + String expectedContent = param1Part + param2Part + param3Part + filePart; + this.snippet.expectHttpRequest("multipart-post").withContents( + httpRequest(POST, "/upload") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", + "multipart/form-data; boundary=" + BOUNDARY) + .content(expectedContent)); + MockMultipartFile multipartFile = new MockMultipartFile("image", + "documents/images/example.png", null, "<< data >>".getBytes()); + documentHttpRequest("multipart-post").handle( + result(fileUpload("/upload").file(multipartFile) + .param("a", "apple", "avocado").param("b", "banana"))); + } + @Test public void multipartPostWithContentType() throws IOException { - String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - String expectedContent = String.format("--%s%nContent-Disposition: form-data; " - + "name=image%nContent-Type: image/png%n%n<< data >>%n--%s--", boundary, - boundary); + String expectedContent = createPart(String + .format("Content-Disposition: form-data; name=image%nContent-Type: " + + "image/png%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( httpRequest(POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", - "multipart/form-data; boundary=" + boundary) + "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, @@ -216,4 +240,18 @@ public void getRequestWithCustomHost() throws IOException { documentHttpRequest("get-request-custom-host").handle( result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); } + + private String createPart(String content) { + return this.createPart(content, true); + } + + private String createPart(String content, boolean last) { + String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + StringBuilder part = new StringBuilder(); + part.append(String.format("--%s%n%s%n", boundary, content)); + if (last) { + part.append(String.format("--%s--", boundary)); + } + return part.toString(); + } } From b1a6840b528e1cd7557fa135100ca1394578640b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Jul 2015 15:06:29 +0100 Subject: [PATCH 0092/1059] Test that a response that is an array of primitives can be documented Closes gh-96 --- .../restdocs/payload/PayloadDocumentationTests.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 7e99a1457..c96afe0d0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -120,6 +120,18 @@ public void arrayResponseWithFields() throws IOException { fieldWithPath("[]a").description("three")).handle(result(response)); } + @Test + public void arrayResponse() throws IOException { + this.snippet.expectResponseFields("array-response").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]", "String", "one")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("[\"a\", \"b\", \"c\"]"); + documentResponseFields("array-response", fieldWithPath("[]").description("one")) + .handle(result(response)); + } + @Test public void undocumentedRequestField() throws IOException { this.thrown.expect(SnippetGenerationException.class); From 6f4fe4ea644d77d66985c249102c8abd3d44c59d Mon Sep 17 00:00:00 2001 From: izeye Date: Sun, 26 Jul 2015 21:42:38 +0900 Subject: [PATCH 0093/1059] Polish documentation Closes gh-98 --- docs/src/docs/asciidoc/configuration.adoc | 4 +-- docs/src/docs/asciidoc/contributing.adoc | 2 +- .../docs/asciidoc/customizing-responses.adoc | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 26 +++++++++---------- docs/src/docs/asciidoc/getting-started.adoc | 22 ++++++++-------- .../asciidoc/working-with-asciidoctor.adoc | 4 +-- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 8824b28ab..ed9b2103b 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -6,7 +6,7 @@ [[configuration-uris]] === Documented URIs -The default configuration for URIs documented by Spring REST docs is: +The default configuration for URIs documented by Spring REST Docs is: |=== |Setting |Default @@ -37,7 +37,7 @@ include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri- [[configuration-snippet-encoding]] === Snippet encoding -The default encoding used by Asciidoc is `UTF-8`. Spring REST docs adopts the same +The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same default for the snippets that it generated. If you require an encoding other than `UTF-8`, use `RestDocumentationConfigurer` to configure it: diff --git a/docs/src/docs/asciidoc/contributing.adoc b/docs/src/docs/asciidoc/contributing.adoc index 04b7f4505..660c3c09e 100644 --- a/docs/src/docs/asciidoc/contributing.adoc +++ b/docs/src/docs/asciidoc/contributing.adoc @@ -1,7 +1,7 @@ [[contributing]] == Contributing -Spring REST Docs is intended to make is easy for you to produce high-quality documentation +Spring REST Docs is intended to make it easy for you to produce high-quality documentation for your RESTful services. However, we can't achieve that goal without your contributions. diff --git a/docs/src/docs/asciidoc/customizing-responses.adoc b/docs/src/docs/asciidoc/customizing-responses.adoc index 08613f938..fe2eb415f 100644 --- a/docs/src/docs/asciidoc/customizing-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-responses.adoc @@ -16,7 +16,7 @@ include::{examples-dir}/com/example/ResponsePostProcessing.java[tags=general] ---- <1> Call `modifyResponseTo` to configure response modifications, passing in one or more `ResponsePostProcessor` implementations. -<2> Proceed with documenting the call +<2> Proceed with documenting the call. [[customizing-responses-pretty-printing]] diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d2ac40571..a7652ed40 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -15,17 +15,17 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: ---- include::{examples-dir}/com/example/Hypermedia.java[tag=links] ---- -<1> `withLinks` is used to describe the expected links -<2> Expects a link whose rel is `alpha`. Uses the static `linkWithRel` method on +<1> Use `withLinks` is to describe the expected links. +<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<3> Expects a link whose rel is `bravo` +<3> Expect a link whose rel is `bravo`. The result is a snippet named `links.adoc` that contains a table describing the resource's links. When documenting links, the test will fail if an undocumented link is found in the response. Similarly, the test will also fail if a documented link is not found in the -response and the link has not be marked as optional. +response and the link has not been marked as optional. @@ -46,7 +46,7 @@ provide one of the built-in `LinkExtractor` implementations to `withLinks`. For ---- include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] ---- -<1> Indicate that the links are in HAL format using the `halLinks` static method on +<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.LinkExtractors`. If your API represents its links in a format other than Atom or HAL you can provide your @@ -64,11 +64,11 @@ provided: ---- include::{examples-dir}/com/example/Payload.java[tags=response] ---- -<1> `withResponseFields` is used to describe the expected fields in the response payload. +<1> Use `withResponseFields` to describe the expected fields in the response payload. To document a request `withRequestFields` can be used. -<2> Expects a field with the path `contact`. Uses the static `fieldWithPath` method on +<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expects a field with the path `contact.email` +<3> Expect a field with the path `contact.email`. The result is a snippet that contains a table describing the fields. For requests this snippet is named `request-fields.adoc`. For responses this snippet is named @@ -76,7 +76,7 @@ snippet is named `request-fields.adoc`. For responses this snippet is named When documenting fields, the test will fail if an undocumented field is found in the payload. Similarly, the test will also fail if a documented field is not found in the -payload and the field has not be marked as optional. For payloads with a hierarchical +payload and the field has not been marked as optional. For payloads with a hierarchical structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. @@ -133,7 +133,7 @@ The following paths are all present: [[documenting-your-api-request-response-payloads-field-types]] ==== Field types -When a field is documented, Spring REST docs will attempt to determine its type by +When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: [cols="1,3"] @@ -182,10 +182,10 @@ A request's query parameters can be documented using `withQueryParameters` ---- include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters] ---- -<1> `withQueryParameters` is used to describe the query parameters -<2> Documents a parameter named `page`. Uses the static `parameterWithName` method on +<1> Use `withQueryParameters` to describe the query parameters. +<2> Document a parameter named `page`. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. -<3> Documents a parameter named `per_page` +<3> Document a parameter named `per_page`. The result is a snippet named `query-parameters.adoc` that contains a table describing the query parameters that are supported by the resource. diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 4b00b430b..bbb0af76a 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -1,7 +1,7 @@ [[getting-started]] == Getting started -This section describes how to get started with Spring REST docs. +This section describes how to get started with Spring REST Docs. @@ -66,9 +66,9 @@ use as a reference. The key parts of the configuration are described below. dependsOn test } ---- -<1> Apply the Asciidoctor plugin -<2> Add a dependency on spring-restdocs in the `testCompile` configuration: -<3> Configure a property to define the output location for generated snippets: +<1> Apply the Asciidoctor plugin. +<2> Add a dependency on spring-restdocs in the `testCompile` configuration. +<3> Configure a property to define the output location for generated snippets. <4> Configure the `test` task with the `org.springframework.restdocs.outputDir` system property. This property controls the location into which Spring REST Docs will write the snippets that it generates. @@ -159,12 +159,12 @@ use as a reference. The key parts of the configuration are described below. ---- -<1> Add a dependency on `spring-restdocs` in the `test` scope -<2> Configure a property to define the output location for generated snippets +<1> Add a dependency on `spring-restdocs` in the `test` scope. +<2> Configure a property to define the output location for generated snippets. <3> Configure the SureFire plugin with the `org.springframework.restdocs.outputDir` system -property. This property controls the location into which Spring REST docs will write the +property. This property controls the location into which Spring REST Docs will write the snippets that it generates. The plugin is also configured to include files whose names end -with `Documentation.java`: +with `Documentation.java`. <4> Configure the Asciidoctor plugin and define an attribute named `snippets`. You can then use this attribute when including the generated snippets in your documentation. <5> [[getting-started-build-configuration-maven-plugin-phase]] If you want to @@ -216,7 +216,7 @@ be included in the project's jar: ---- -<1> The existing declaration for the Asciidoctor plugin +<1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound to the same phase and the resource plugin must run after the Asciidoctor plugin. @@ -260,8 +260,8 @@ service and document the request and response. include::{examples-dir}/com/example/InvokeService.java[tags=invoke-service] ---- <1> Invoke the root (`/`) of the service an indicate that an `application/json` response -is required -<2> Assert that the service is produced the expected response +is required. +<2> Assert that the service is produced the expected response. <3> Document the call to the service, writing the snippets into a directory named `index` that will be located beneath the configured output directory. The snippets are written by a `RestDocumentationResultHandler`. An instance of this class can be obtained from the diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index c942ad233..dd0d68d24 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -48,7 +48,7 @@ example, the widths of a table's columns can be specified using the `cols` attri [source,adoc,indent=0] ---- [cols=1,3] <1> -\include::{snippets}index/links.adoc[] +\include::{snippets}/index/links.adoc[] ---- <1> The table's width will be split across its two columns with the second column being three times as wide as the first. @@ -63,7 +63,7 @@ The title of a table can be specified using a line prefixed by a `.`: [source,adoc,indent=0] ---- .Links <1> -\include::{snippets}index/links.adoc[] +\include::{snippets}/index/links.adoc[] ---- <1> The table's title will be `Links`. From 4f850cddd920ca1425c2ae05afff88287dce7b4f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Jul 2015 15:38:39 +0100 Subject: [PATCH 0094/1059] Use a template engine to produce the documentation snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new TemplateEngine abstraction that is used to produce the documentation snippets. A default JMustache-based implementation is provided. JMustache has been repackaged and embedded to prevent unwanted conflicts and side-effects. By default, snippet templates are loaded from the classpath in the org.springframework.restdocs.templates package. Default snippet templates are provided for all of the snippets that can be generated. Each of these templates is named after the snippet that it will produce – the snippet {name}.adoc is produced by the snippet template default-{name}.snippet. A snippet named {name}.snippet, if present, will be used in preference to the default snippet, thereby allowing the default snippets to be overriden. --- build.gradle | 2 + .../docs/asciidoc/customizing-snippets.adoc | 12 + docs/src/docs/asciidoc/index.adoc | 5 +- .../asciidoc/working-with-asciidoctor.adoc | 4 +- spring-restdocs/build.gradle | 35 ++- .../config/RestDocumentationConfigurer.java | 29 ++- .../restdocs/curl/CurlDocumentation.java | 120 +++++----- .../restdocs/http/HttpDocumentation.java | 221 ++++++++++-------- .../hypermedia/LinkSnippetResultHandler.java | 32 +-- .../payload/FieldSnippetResultHandler.java | 87 +++---- .../QueryParametersSnippetResultHandler.java | 32 +-- .../restdocs/snippet/AsciidoctorWriter.java | 101 -------- .../restdocs/snippet/DocumentationWriter.java | 125 ---------- .../snippet/SnippetWritingResultHandler.java | 5 +- .../StandardTemplateResourceResolver.java | 49 ++++ .../restdocs/templates/Template.java | 22 ++ .../restdocs/templates/TemplateEngine.java | 42 ++++ .../templates/TemplateResourceResolver.java | 37 +++ .../templates/mustache/MustacheTemplate.java | 47 ++++ .../mustache/MustacheTemplateEngine.java | 60 +++++ .../templates/default-curl-request.snippet | 4 + .../restdocs/templates/default-fields.snippet | 10 + .../templates/default-http-request.snippet | 8 + .../templates/default-http-response.snippet | 8 + .../restdocs/templates/default-links.snippet | 9 + .../default-query-parameters.snippet | 9 + .../templates/default-request-fields.snippet | 10 + .../templates/default-response-fields.snippet | 10 + .../RestDocumentationIntegrationTests.java | 26 +++ .../snippet/AsciidoctorWriterTests.java | 86 ------- .../restdocs/test/SnippetMatchers.java | 21 +- .../restdocs/test/StubMvcResult.java | 5 + .../restdocs/templates/curl-request.snippet | 1 + 33 files changed, 705 insertions(+), 569 deletions(-) create mode 100644 docs/src/docs/asciidoc/customizing-snippets.adoc delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index 3617a1407..202910b91 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ apply plugin: 'samples' ext { springVersion = '4.1.5.RELEASE' + jmustacheVersion = '1.10' } subprojects { @@ -28,6 +29,7 @@ subprojects { } dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' + dependency "com.samskivert:jmustache:$jmustacheVersion" dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'junit:junit:4.12' dependency 'org.hamcrest:hamcrest-core:1.3' diff --git a/docs/src/docs/asciidoc/customizing-snippets.adoc b/docs/src/docs/asciidoc/customizing-snippets.adoc new file mode 100644 index 000000000..94cca79de --- /dev/null +++ b/docs/src/docs/asciidoc/customizing-snippets.adoc @@ -0,0 +1,12 @@ +[[customizing-snippets]] +== Customizing the generated snippets + +Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the +generated snippets. You can customize the generated snippets by overriding the +{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[default +templates]. + +Templates are loaded from the classpath in the `org.springframework.restdocs.templates` +package and each template is named after the snippet that it will produce. For example, to +override the template for the `curl-request.adoc` snippet, create a template named +`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 99ec9d5f7..f6afcf0ed 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -9,7 +9,9 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs -:samples: {github}/tree/{branch-or-tag}/samples +:source: {github}/tree/{branch-or-tag}/ +:samples: {source}/samples +:templates: {source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle @@ -22,6 +24,7 @@ include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] +include::customizing-snippets.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index dd0d68d24..ce166410b 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -33,8 +33,8 @@ can be used to reference the snippets output directory, for example: === Customizing tables Many of the snippets contain a table in its default configuration. The appearance of the -table can be customized by providing some additional configuration when the snippet is -included. +table can be customized, either by providing some additional configuration when the +snippet is included or by using a custom snippet template. diff --git a/spring-restdocs/build.gradle b/spring-restdocs/build.gradle index 65056ff47..e4c2fd502 100644 --- a/spring-restdocs/build.gradle +++ b/spring-restdocs/build.gradle @@ -29,6 +29,8 @@ sonarRunner { configurations { jacoco + jarjar + jmustache } ext { @@ -38,6 +40,24 @@ ext { ] as String[] } +task jmustacheRepackJar(type: Jar) { repackJar -> + repackJar.baseName = "restdocs-jmustache-repack" + repackJar.version = jmustacheVersion + + doLast() { + project.ant { + taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", + classpath: configurations.jarjar.asPath + jarjar(destfile: repackJar.archivePath) { + configurations.jmustache.each { originalJar -> + zipfileset(src: originalJar, includes: '**/*.class') + } + rule(pattern: 'com.samskivert.**', result: 'org.springframework.restdocs.@1') + } + } + } +} + dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' compile 'junit:junit' @@ -46,13 +66,23 @@ dependencies { compile 'org.springframework:spring-web' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' + compile files(jmustacheRepackJar) jacoco 'org.jacoco:org.jacoco.agent::runtime' + jarjar 'com.googlecode.jarjar:jarjar:1.3' + jmustache 'com.samskivert:jmustache@jar' testCompile 'org.springframework.hateoas:spring-hateoas' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' } +jar { + dependsOn jmustacheRepackJar + from(zipTree(jmustacheRepackJar.archivePath)) { + include "org/springframework/restdocs/**" + } +} + task sourcesJar(type: Jar) { classifier = 'sources' from project.sourceSets.main.allSource @@ -60,13 +90,12 @@ task sourcesJar(type: Jar) { javadoc { description = "Generates project-level javadoc for use in -javadoc jar" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.author = true options.header = "Spring REST Docs $version" options.docTitle = "${options.header} API" options.links(project.ext.javadocLinks) - options.addStringOption('-quiet') + options.addStringOption '-quiet' } task javadocJar(type: Jar) { @@ -80,7 +109,7 @@ artifacts { } test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*,excludes=org.springframework.restdocs.mustache.*" testLogging { exceptionFormat "full" } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 8d5781462..c8757d189 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,6 +21,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -44,6 +47,8 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { private final RequestPostProcessor requestPostProcessor; + private TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + /** * Creates a new {@link RestDocumentationConfigurer}. * @see RestDocumentation#documentationConfiguration() @@ -52,7 +57,8 @@ public RestDocumentationConfigurer() { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( Arrays. asList(this.uriConfigurer, this.snippetConfigurer, new StepCountConfigurer(), - new ContentLengthHeaderConfigurer())); + new ContentLengthHeaderConfigurer(), + new TemplateEngineConfigurer())); } public UriConfigurer uris() { @@ -63,6 +69,11 @@ public SnippetConfigurer snippets() { return this.snippetConfigurer; } + public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) { + this.templateEngineConfigurer.setTemplateEngine(templateEngine); + return this; + } + @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { @@ -95,6 +106,22 @@ void apply(MockHttpServletRequest request) { } + private static class TemplateEngineConfigurer extends AbstractConfigurer { + + private TemplateEngine templateEngine = new MustacheTemplateEngine( + new StandardTemplateResourceResolver()); + + @Override + void apply(MockHttpServletRequest request) { + request.setAttribute(TemplateEngine.class.getName(), this.templateEngine); + } + + void setTemplateEngine(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + } + private static class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 65368dc6d..e3161c521 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -17,12 +17,15 @@ package org.springframework.restdocs.curl; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; 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.DocumentationAction; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -50,18 +53,11 @@ private CurlDocumentation() { * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentCurlRequest(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "curl-request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result)); - } - }; + return new CurlRequestWritingResultHandler(outputDir); } - private static final class CurlRequestDocumentationAction implements - DocumentationAction { + private static final class CurlRequestWritingResultHandler extends + SnippetWritingResultHandler { private static final String SCHEME_HTTP = "http"; @@ -71,46 +67,52 @@ private static final class CurlRequestDocumentationAction implements private static final int STANDARD_PORT_HTTPS = 443; - private final DocumentationWriter writer; - - private final MvcResult result; - - CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + private CurlRequestWritingResultHandler(String outputDir) { + super(outputDir, "curl-request"); } @Override - public void perform() throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - this.result.getRequest()); + public void handle(MvcResult result, PrintWriter writer) throws IOException { + Map context = new HashMap(); + context.put("language", "bash"); + context.put("arguments", getCurlCommandArguments(result)); - this.writer.print("curl '"); + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); - writeAuthority(request); - writePathAndQueryString(request); + writer.print(templateEngine.compileTemplate("curl-request").render(context)); + } + + private String getCurlCommandArguments(MvcResult result) throws IOException { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + result.getRequest()); - this.writer.print("'"); + printer.print("'"); + writeAuthority(request, printer); + writePathAndQueryString(request, printer); + printer.print("'"); - writeOptionToIncludeHeadersInOutput(); - writeHttpMethodIfNecessary(request); - writeHeaders(request); + writeOptionToIncludeHeadersInOutput(printer); + writeHttpMethodIfNecessary(request, printer); + writeHeaders(request, printer); if (request.isMultipartRequest()) { - writeParts(request); + writeParts(request, printer); } - writeContent(request); + writeContent(request, printer); - this.writer.println(); + return command.toString(); } - private void writeAuthority(DocumentableHttpServletRequest request) { - this.writer.print(String.format("%s://%s", request.getScheme(), - request.getHost())); + private void writeAuthority(DocumentableHttpServletRequest request, + PrintWriter writer) { + writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); if (isNonStandardPort(request)) { - this.writer.print(String.format(":%d", request.getPort())); + writer.print(String.format(":%d", request.getPort())); } } @@ -119,75 +121,75 @@ private boolean isNonStandardPort(DocumentableHttpServletRequest request) { || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); } - private void writePathAndQueryString(DocumentableHttpServletRequest request) { + private void writePathAndQueryString(DocumentableHttpServletRequest request, + PrintWriter writer) { if (StringUtils.hasText(request.getContextPath())) { - this.writer.print(String.format( + writer.print(String.format( request.getContextPath().startsWith("/") ? "%s" : "/%s", request.getContextPath())); } - this.writer.print(request.getRequestUriWithQueryString()); + writer.print(request.getRequestUriWithQueryString()); } - private void writeOptionToIncludeHeadersInOutput() { - this.writer.print(" -i"); + private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { + writer.print(" -i"); } - private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request) { + private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request, + PrintWriter writer) { if (!request.isGetRequest()) { - this.writer.print(String.format(" -X %s", request.getMethod())); + writer.print(String.format(" -X %s", request.getMethod())); } } - private void writeHeaders(DocumentableHttpServletRequest request) { + private void writeHeaders(DocumentableHttpServletRequest request, + PrintWriter writer) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { - this.writer.print(String.format(" -H '%s: %s'", entry.getKey(), - header)); + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } } - private void writeParts(DocumentableHttpServletRequest request) + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) throws IOException { for (Entry> entry : request.getMultipartFiles() .entrySet()) { for (MultipartFile file : entry.getValue()) { - this.writer.printf(" -F '%s=", file.getName()); + writer.printf(" -F '%s=", file.getName()); if (!StringUtils.hasText(file.getOriginalFilename())) { - this.writer.append(new String(file.getBytes())); + writer.append(new String(file.getBytes())); } else { - this.writer.printf("@%s", file.getOriginalFilename()); + writer.printf("@%s", file.getOriginalFilename()); } if (StringUtils.hasText(file.getContentType())) { - this.writer.append(";type=").append(file.getContentType()); + writer.append(";type=").append(file.getContentType()); } - this.writer.append("'"); + writer.append("'"); } } } - private void writeContent(DocumentableHttpServletRequest request) - throws IOException { + private void writeContent(DocumentableHttpServletRequest request, + PrintWriter writer) throws IOException { if (request.getContentLength() > 0) { - this.writer - .print(String.format(" -d '%s'", request.getContentAsString())); + writer.print(String.format(" -d '%s'", request.getContentAsString())); } else if (request.isMultipartRequest()) { for (Entry entry : request.getParameterMap().entrySet()) { for (String value : entry.getValue()) { - this.writer.print(String.format(" -F '%s=%s'", entry.getKey(), - value)); + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); } } } else if (request.isPostRequest() || request.isPutRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { - this.writer.print(String.format(" -d '%s'", queryString)); + writer.print(String.format(" -d '%s'", queryString)); } } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 025f17ed3..2f7448fdf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -17,15 +17,19 @@ package org.springframework.restdocs.http; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -39,6 +43,8 @@ */ public abstract class HttpDocumentation { + private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + private HttpDocumentation() { } @@ -51,15 +57,7 @@ private HttpDocumentation() { * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentHttpRequest(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "http-request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new HttpRequestDocumentationAction(writer, - result)); - } - }; + return new HttpRequestWritingResultHandler(outputDir); } /** @@ -70,157 +68,188 @@ public void handle(MvcResult result, DocumentationWriter writer) * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentHttpResponse(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "http-response") { + return new HttpResponseWritingResultHandler(outputDir); - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new HttpResponseDocumentationAction(writer, - result)); - } - }; } - private static class HttpRequestDocumentationAction implements DocumentationAction { + private static final class HttpRequestWritingResultHandler extends + SnippetWritingResultHandler { - private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + private HttpRequestWritingResultHandler(String outputDir) { + super(outputDir, "http-request"); + } - private final DocumentationWriter writer; + @Override + public void handle(MvcResult result, PrintWriter writer) throws IOException { + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + result.getRequest()); + Map context = new HashMap(); + context.put("language", "http"); + context.put("method", result.getRequest().getMethod()); + context.put("path", request.getRequestUriWithQueryString()); + context.put("headers", getHeaders(request)); + context.put("requestBody", getRequestBody(request)); - private final MvcResult result; + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); - HttpRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + writer.print(templateEngine.compileTemplate("http-request").render(context)); } - @Override - public void perform() throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - this.result.getRequest()); - this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), - request.getRequestUriWithQueryString()); + private List> getHeaders( + DocumentableHttpServletRequest request) { + List> headers = new ArrayList<>(); if (requiresHostHeader(request)) { - writeHeader(HttpHeaders.HOST, request.getHost()); + headers.add(header(HttpHeaders.HOST, request.getHost())); } + for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { if (header.getKey() == HttpHeaders.CONTENT_TYPE && request.isMultipartRequest()) { - writeHeader(header.getKey(), String.format("%s; boundary=%s", - value, MULTIPART_BOUNDARY)); + headers.add(header(header.getKey(), String.format( + "%s; boundary=%s", value, MULTIPART_BOUNDARY))); } else { - this.writer.printf("%s: %s%n", header.getKey(), value); + headers.add(header(header.getKey(), value)); } } } if (requiresFormEncodingContentType(request)) { - writeHeader(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE); + headers.add(header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)); } - this.writer.println(); + return headers; + } + + private String getRequestBody(DocumentableHttpServletRequest request) + throws IOException { + StringWriter httpRequest = new StringWriter(); + PrintWriter writer = new PrintWriter(httpRequest); if (request.getContentLength() > 0) { - this.writer.println(request.getContentAsString()); + writer.println(); + writer.print(request.getContentAsString()); } else if (request.isPostRequest() || request.isPutRequest()) { if (request.isMultipartRequest()) { - writeParts(request); + writeParts(request, writer); } else { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { - this.writer.println(queryString); + writer.println(); + writer.print(queryString); } } } + return httpRequest.toString(); } - private boolean requiresHostHeader(DocumentableHttpServletRequest request) { - return request.getHeaders().get(HttpHeaders.HOST) == null; - } - - private boolean requiresFormEncodingContentType( - DocumentableHttpServletRequest request) { - return request.getHeaders().getContentType() == null - && (request.isPostRequest() || request.isPutRequest()) - && StringUtils.hasText(request.getParameterMapAsQueryString()); - } - - private void writeHeader(String name, String value) { - this.writer.printf("%s: %s%n", name, value); - } - - private void writeParts(DocumentableHttpServletRequest request) + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) throws IOException { + writer.println(); for (Entry parameter : request.getParameterMap().entrySet()) { for (String value : parameter.getValue()) { - writePartBoundary(); - writePart(parameter.getKey(), value, null); - this.writer.println(); + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, writer); + writer.println(); } } for (Entry> entry : request.getMultipartFiles() .entrySet()) { for (MultipartFile file : entry.getValue()) { - writePartBoundary(); - writePart(file); - this.writer.println(); + writePartBoundary(writer); + writePart(file, writer); + writer.println(); } } - writeMultipartEnd(); + writeMultipartEnd(writer); } - private void writePartBoundary() { - this.writer.printf("--%s%n", MULTIPART_BOUNDARY); + private void writePartBoundary(PrintWriter writer) { + writer.printf("--%s%n", MULTIPART_BOUNDARY); } - private void writePart(String name, String value, String contentType) { - this.writer.printf("Content-Disposition: form-data; name=%s%n", name); + private void writePart(String name, String value, String contentType, + PrintWriter writer) { + writer.printf("Content-Disposition: form-data; name=%s%n", name); if (StringUtils.hasText(contentType)) { - this.writer.printf("Content-Type: %s%n", contentType); + writer.printf("Content-Type: %s%n", contentType); } - this.writer.println(); - this.writer.print(value); + writer.println(); + writer.print(value); } - private void writePart(MultipartFile part) throws IOException { - writePart(part.getName(), new String(part.getBytes()), part.getContentType()); + private void writePart(MultipartFile part, PrintWriter writer) throws IOException { + writePart(part.getName(), new String(part.getBytes()), part.getContentType(), + writer); } - private void writeMultipartEnd() { - this.writer.printf("--%s--%n", MULTIPART_BOUNDARY); + private void writeMultipartEnd(PrintWriter writer) { + writer.printf("--%s--", MULTIPART_BOUNDARY); + } + + private boolean requiresHostHeader(DocumentableHttpServletRequest request) { + return request.getHeaders().get(HttpHeaders.HOST) == null; } - } - private static final class HttpResponseDocumentationAction implements - DocumentationAction { + private boolean requiresFormEncodingContentType( + DocumentableHttpServletRequest request) { + return request.getHeaders().getContentType() == null + && (request.isPostRequest() || request.isPutRequest()) + && StringUtils.hasText(request.getParameterMapAsQueryString()); + } - private final DocumentationWriter writer; + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; + } + } - private final MvcResult result; + private static final class HttpResponseWritingResultHandler extends + SnippetWritingResultHandler { - HttpResponseDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + private HttpResponseWritingResultHandler(String outputDir) { + super(outputDir, "http-response"); } @Override - public void perform() throws IOException { - HttpStatus status = HttpStatus.valueOf(this.result.getResponse().getStatus()); - this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), - status.getReasonPhrase())); - for (String headerName : this.result.getResponse().getHeaderNames()) { - for (String header : this.result.getResponse().getHeaders(headerName)) { - this.writer.println(String.format("%s: %s", headerName, header)); + public void handle(MvcResult result, PrintWriter writer) throws IOException { + HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); + Map context = new HashMap(); + context.put( + "responseBody", + StringUtils.hasLength(result.getResponse().getContentAsString()) ? String + .format("%n%s", result.getResponse().getContentAsString()) + : ""); + context.put("language", "http"); + context.put("statusCode", status.value()); + context.put("statusReason", status.getReasonPhrase()); + + List> headers = new ArrayList<>(); + context.put("headers", headers); + + for (String headerName : result.getResponse().getHeaderNames()) { + for (String header : result.getResponse().getHeaders(headerName)) { + headers.add(header(headerName, header)); } } - this.writer.println(); - String content = this.result.getResponse().getContentAsString(); - if (StringUtils.hasText(content)) { - this.writer.println(content); - } + + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + + writer.print(templateEngine.compileTemplate("http-response").render(context)); + + } + + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 4702930a1..6d18c6259 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -17,18 +17,17 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -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.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -61,10 +60,9 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { validate(extractLinks(result)); - writeDocumentationSnippet(writer); + writeDocumentationSnippet(result, writer); } private Map> extractLinks(MvcResult result) throws IOException { @@ -111,19 +109,13 @@ private void validate(Map> links) { } } - private void writeDocumentationSnippet(DocumentationWriter writer) throws IOException { - writer.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("Relation", "Description"); - for (Entry entry : LinkSnippetResultHandler.this.descriptorsByRel - .entrySet()) { - tableWriter.row(entry.getKey(), entry.getValue().getDescription()); - } - } - - }); + private void writeDocumentationSnippet(MvcResult result, PrintWriter writer) + throws IOException { + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + context.put("links", this.descriptorsByRel.values()); + writer.print(templateEngine.compileTemplate("links").render(context)); } } \ No newline at end of file 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 index 84627c420..1e87a5519 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -17,16 +17,17 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.io.PrintWriter; import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; 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.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -35,7 +36,7 @@ /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * resource's request or response fields. - * + * * @author Andreas Evers * @author Andy Wilkinson */ @@ -49,11 +50,14 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private final ObjectMapper objectMapper = new ObjectMapper(); + private final String templateName; + private List fieldDescriptors; - FieldSnippetResultHandler(String outputDir, String filename, + FieldSnippetResultHandler(String outputDir, String type, List descriptors) { - super(outputDir, filename + "-fields"); + super(outputDir, type + "-fields"); + this.templateName = type + "-fields"; for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); @@ -63,49 +67,46 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - final Object payload = extractPayload(result); - - 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 = getFieldType(descriptor, payload); - tableWriter.row(entry.getKey().toString(), type.toString(), entry - .getValue().getDescription()); - } + Object payload = extractPayload(result); + + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + List> fields = new ArrayList<>(); + context.put("fields", fields); + for (Entry entry : this.descriptorsByPath.entrySet()) { + FieldDescriptor descriptor = entry.getValue(); + FieldType type = getFieldType(descriptor, payload); + Map fieldModel = new HashMap<>(); + fieldModel.put("path", entry.getKey()); + fieldModel.put("type", type.toString()); + fieldModel.put("description", descriptor.getDescription()); + fields.add(fieldModel); + } + writer.print(templateEngine.compileTemplate(this.templateName).render(context)); + } + private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { + if (descriptor.getType() != null) { + return descriptor.getType(); + } + else { + try { + return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( + descriptor.getPath(), payload); } - - private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - if (descriptor.getType() != null) { - return descriptor.getType(); - } - else { - try { - return FieldSnippetResultHandler.this.fieldTypeResolver - .resolveFieldType(descriptor.getPath(), payload); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the" - + " payload. Please provide a type using" - + " FieldDescriptor.type(FieldType)."; - throw new FieldTypeRequiredException(message); - } - } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the" + + " payload. Please provide a type using" + + " FieldDescriptor.type(FieldType)."; + throw new FieldTypeRequiredException(message); } - - }); - + } } private Object extractPayload(MvcResult result) throws IOException { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index 1058b0a8c..8813d0c8d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -17,17 +17,16 @@ package org.springframework.restdocs.request; import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -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.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -52,10 +51,9 @@ protected QueryParametersSnippetResultHandler(String outputDir, } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { verifyParameterDescriptors(result); - documentParameters(writer); + documentParameters(result, writer); } private void verifyParameterDescriptors(MvcResult result) { @@ -87,19 +85,13 @@ private void verifyParameterDescriptors(MvcResult result) { Assert.isTrue(actualParameters.equals(expectedParameters)); } - private void documentParameters(DocumentationWriter writer) throws IOException { - writer.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("Parameter", "Description"); - for (Entry entry : QueryParametersSnippetResultHandler.this.descriptorsByName - .entrySet()) { - tableWriter.row(entry.getKey(), entry.getValue().getDescription()); - } - } - - }); + private void documentParameters(MvcResult result, PrintWriter writer) + throws IOException { + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + context.put("parameters", this.descriptorsByName.values()); + writer.print(templateEngine.compileTemplate("query-parameters").render(context)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java deleted file mode 100644 index fe35ef426..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java +++ /dev/null @@ -1,101 +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.snippet; - -import java.io.IOException; -import java.io.Writer; - -/** - * A {@link DocumentationWriter} that produces output in Asciidoctor. - * - * @author Andy Wilkinson - */ -public class AsciidoctorWriter extends DocumentationWriter { - - private static final String DELIMITER_CODE_BLOCK = "----"; - - private static final String DELIMITER_TABLE = "|==="; - - private final TableWriter tableWriter = new AsciidoctorTableWriter(); - - /** - * Creates a new {@code AsciidoctorWriter} that will write to the given {@code writer} - * @param writer The writer to which output will be written - */ - public AsciidoctorWriter(Writer writer) { - super(writer); - } - - @Override - public void shellCommand(final DocumentationAction action) throws IOException { - codeBlock("bash", new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriter.this.print("$ "); - action.perform(); - } - }); - } - - @Override - public void codeBlock(String language, DocumentationAction action) throws IOException { - println(); - if (language != null) { - println("[source," + language + "]"); - } - println(DELIMITER_CODE_BLOCK); - action.perform(); - println(DELIMITER_CODE_BLOCK); - println(); - } - - @Override - public void table(TableAction action) throws IOException { - println(); - println(DELIMITER_TABLE); - action.perform(this.tableWriter); - println(DELIMITER_TABLE); - println(); - } - - private final class AsciidoctorTableWriter implements TableWriter { - - @Override - public void headers(String... headers) { - StringBuilder builder = new StringBuilder(); - for (String header : headers) { - builder.append("|"); - builder.append(header); - } - println(builder.toString()); - println(); - } - - @Override - public void row(String... entries) { - for (String entry : entries) { - print("|"); - println(entry); - } - println(); - } - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java deleted file mode 100644 index b56344016..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java +++ /dev/null @@ -1,125 +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.snippet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Writer; - -/** - * A {@link PrintWriter} that provides additional methods which are useful for producing - * API documentation. - * - * @author Andy Wilkinson - */ -public abstract class DocumentationWriter extends PrintWriter { - - protected DocumentationWriter(Writer writer) { - super(writer, true); - } - - /** - * Calls the given {@code action} to document a shell command. Any prefix necessary - * for the documentation format is written prior to calling the {@code action}. Having - * called the action, any necessary suffix is then written. - * - * @param action the action that will produce the shell command - * @throws IOException if the documentation fails - */ - public abstract void shellCommand(DocumentationAction action) throws IOException; - - /** - * Calls the given {@code action} to document a code block. The code block will be - * annotated as containing code written in the given {@code language}. Any prefix - * necessary for the documentation format is written prior to calling the - * {@code action}. Having called the {@code action}, any necessary suffix is then - * written. - * - * @param language the language in which the code is written - * @param action the action that will produce the code - * @throws IOException if the documentation fails - */ - public abstract void codeBlock(String language, DocumentationAction action) - throws IOException; - - /** - * Calls the given {@code action} to document a table. Any prefix necessary for - * documenting a table is written prior to calling the {@code action}. Having called - * the {@code action}, any necessary suffix is then written. - * - * @param action the action that will produce the table - * @throws IOException if the documentation fails - */ - public abstract void table(TableAction action) throws IOException; - - /** - * Encapsulates an action that outputs some documentation. Typically implemented as a - * lamda or, pre-Java 8, as an anonymous inner class. - * - * @see DocumentationWriter#shellCommand - * @see DocumentationWriter#codeBlock - */ - public interface DocumentationAction { - - /** - * Perform the encapsulated action - * - * @throws IOException if the action fails - */ - void perform() throws IOException; - - } - - /** - * Encapsulates an action that outputs a table. - * - * @see DocumentationWriter#table(TableAction) - */ - public interface TableAction { - - /** - * Perform the encapsulated action - * - * @param tableWriter the writer to be used to write the table - * @throws IOException if the action fails - */ - void perform(TableWriter tableWriter) throws IOException; - - } - - /** - * A writer for producing a table - */ - public interface TableWriter { - - /** - * Writes the table's headers - * - * @param headers the headers - */ - void headers(String... headers); - - /** - * Writes a row in the table - * - * @param entries the entries in the row - */ - void row(String... entries); - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 1bd057c9e..1a1dc0220 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -21,6 +21,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.io.Writer; import org.springframework.restdocs.config.RestDocumentationContext; @@ -43,13 +44,13 @@ protected SnippetWritingResultHandler(String outputDir, String fileName) { this.fileName = fileName; } - protected abstract void handle(MvcResult result, DocumentationWriter writer) + protected abstract void handle(MvcResult result, PrintWriter writer) throws IOException; @Override public void handle(MvcResult result) throws IOException { try (Writer writer = createWriter()) { - handle(result, new AsciidoctorWriter(writer)); + handle(result, new PrintWriter(writer)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java new file mode 100644 index 000000000..eb8fb455e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -0,0 +1,49 @@ +/* + * 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.templates; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * Standard implementation of {@link TemplateResourceResolver}. + *

+ * Templates are resolved by first looking for a resource on the classpath named + * {@code org/springframework/restdocs/templates/{name}.snippet}. If no such + * resource exists {@code default-} is prepended to the name and the classpath is checked + * again. The built-in snippet templates are all named {@code default- name}, thereby + * allowing them to be overridden. + * + * @author Andy Wilkinson + */ +public class StandardTemplateResourceResolver implements TemplateResourceResolver { + + @Override + public Resource resolveTemplateResource(String name) { + ClassPathResource classPathResource = new ClassPathResource( + "org/springframework/restdocs/templates/" + name + ".snippet"); + if (!classPathResource.exists()) { + classPathResource = new ClassPathResource( + "org/springframework/restdocs/templates/default-" + name + ".snippet"); + if (!classPathResource.exists()) { + throw new IllegalStateException("Template named '" + name + + "' could not be resolved"); + } + } + return classPathResource; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java new file mode 100644 index 000000000..302e5f6ca --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java @@ -0,0 +1,22 @@ +package org.springframework.restdocs.templates; + +import java.util.Map; + +/** + * A compiled {@code Template} that can be rendered to a {@link String}. + * + * @author Andy Wilkinson + * + */ +public interface Template { + + /** + * Renders the template to a {@link String} using the given {@code context} for + * variable/property resolution. + * + * @param context The context to use + * @return The rendered template + */ + String render(Map context); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java new file mode 100644 index 000000000..2ab905712 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java @@ -0,0 +1,42 @@ +/* + * 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.templates; + +import java.io.IOException; + +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +/** + * A {@code TemplateEngine} is used to render documentation snippets. + * + * @author Andy Wilkinson + * @see MustacheTemplateEngine + */ +public interface TemplateEngine { + + /** + * Compiles the template at the given {@code path}. Typically, a + * {@link TemplateResourceResolver} will be used to resolve the path into a resource + * that can be read and compiled. + * + * @param path the path of the template + * @return the compiled {@code Template} + * @throws IOException if compilation fails + */ + Template compileTemplate(String path) throws IOException; + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java new file mode 100644 index 000000000..d4259af1c --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java @@ -0,0 +1,37 @@ +/* + * 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.templates; + +import org.springframework.core.io.Resource; + +/** + * A {@code TemplateResourceResolver} is responsible for resolving a name for a template + * into a {@link Resource} from which the template can be read. + * + * @author Andy Wilkinson + */ +public interface TemplateResourceResolver { + + /** + * Resolves a {@link Resource} for the template with the given {@code name}. + * + * @param name the name of the template + * @return the {@code Resource} from which the template can be read + */ + public Resource resolveTemplateResource(String name); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java new file mode 100644 index 000000000..524217326 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -0,0 +1,47 @@ +/* + * 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.templates.mustache; + +import java.util.Map; + +import org.springframework.restdocs.templates.Template; + +/** + * An adapter that exposes a compiled Mustache + * template as a {@link Template}. + * + * @author Andy Wilkinson + */ +public class MustacheTemplate implements Template { + + private final org.springframework.restdocs.mustache.Template delegate; + + /** + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. When + * rendered, the given {@code defaultContext} will be combined with the render context + * prior to executing the delegate. + * @param delegate The delegate to adapt + */ + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { + this.delegate = delegate; + } + + @Override + public String render(Map context) { + return this.delegate.execute(context); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java new file mode 100644 index 000000000..0b16ae81a --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -0,0 +1,60 @@ +/* + * 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.templates.mustache; + +import java.io.IOException; +import java.io.InputStreamReader; + +import org.springframework.core.io.Resource; +import org.springframework.restdocs.mustache.Mustache; +import org.springframework.restdocs.mustache.Mustache.Compiler; +import org.springframework.restdocs.templates.Template; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; + +/** + * A Mustache-based {@link TemplateEngine} + * implemented using JMustache. + *

+ * Note that JMustache has been repackaged and embedded to prevent classpath conflicts. + * + * @author Andy Wilkinson + */ +public class MustacheTemplateEngine implements TemplateEngine { + + private final Compiler compiler = Mustache.compiler().escapeHTML(false); + + private final TemplateResourceResolver templateResourceResolver; + + /** + * Creates a new {@link MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve template paths. + * + * @param templateResourceResolver The resolve to use + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { + this.templateResourceResolver = templateResourceResolver; + } + + @Override + public Template compileTemplate(String name) throws IOException { + Resource templateResource = this.templateResourceResolver + .resolveTemplateResource(name); + return new MustacheTemplate(this.compiler.compile(new InputStreamReader( + templateResource.getInputStream()))); + } + +} diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet new file mode 100644 index 000000000..8eaef9e12 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl {{arguments}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet new file mode 100644 index 000000000..497f5a03a --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet new file mode 100644 index 000000000..a3e529aed --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet @@ -0,0 +1,8 @@ +[source,http] +---- +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet new file mode 100644 index 000000000..fe095d36e --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet @@ -0,0 +1,8 @@ +[source,http] +---- +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet new file mode 100644 index 000000000..b8b452903 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet @@ -0,0 +1,9 @@ +|=== +|Relation|Description + +{{#links}} +|{{rel}} +|{{description}} + +{{/links}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet new file mode 100644 index 000000000..067e1fb83 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet new file mode 100644 index 000000000..497f5a03a --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet new file mode 100644 index 000000000..497f5a03a --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index a7dc2b94f..e5aa52a61 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.restdocs; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -31,6 +32,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -175,6 +178,29 @@ public void postProcessedResponse() throws Exception { + " \"...\"%n } ]%n}"))))); } + @Test + public void customSnippetTemplate() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + ClassLoader classLoader = new URLClassLoader(new URL[] { new File( + "src/test/resources/custom-snippet-templates").toURI().toURL() }, + getClass().getClassLoader()); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + try { + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("custom-snippet-template")); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + assertThat(new File( + "build/generated-snippets/custom-snippet-template/curl-request.adoc"), + is(snippet().withContents(equalTo("Custom curl request")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java deleted file mode 100644 index 15eb2728e..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.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.snippet; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; - -import org.junit.Test; -import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; - -/** - * Tests for {@link AsciidoctorWriter} - * - * @author Andy Wilkinson - */ -public class AsciidoctorWriterTests { - - private Writer output = new StringWriter(); - - private DocumentationWriter documentationWriter = new AsciidoctorWriter(this.output); - - @Test - public void codeBlock() throws Exception { - this.documentationWriter.codeBlock("java", new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriterTests.this.documentationWriter.println("foo"); - } - }); - - String expectedOutput = String.format("%n[source,java]%n----%nfoo%n----%n%n"); - assertEquals(expectedOutput, this.output.toString()); - } - - @Test - public void shellCommand() throws Exception { - this.documentationWriter.shellCommand(new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriterTests.this.documentationWriter.println("foo"); - } - }); - - String expectedOutput = String.format("%n[source,bash]%n----%n$ foo%n----%n%n"); - assertEquals(expectedOutput, this.output.toString()); - } - - @Test - public void table() throws Exception { - this.documentationWriter.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("One", "Two", "Three"); - tableWriter.row("alpha", "bravo", "charlie"); - tableWriter.row("foo", "bar", "baz"); - } - - }); - String expectedOutput = String - .format("%n|===%n|One|Two|Three%n%n|alpha%n|bravo%n|charlie%n%n|foo%n|bar%n|baz%n%n|===%n%n"); - assertEquals(expectedOutput, this.output.toString()); - System.out.println(this.output.toString()); - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index a8cdb4d05..46eecfcd7 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -22,6 +22,7 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import org.hamcrest.BaseMatcher; @@ -100,8 +101,12 @@ public void describeMismatch(Object item, Description description) { private String getLinesAsString() { StringWriter writer = new StringWriter(); - for (String line : this.lines) { - writer.append(String.format("%s%n", line)); + Iterator iterator = this.lines.iterator(); + while (iterator.hasNext()) { + writer.append(String.format("%s", iterator.next())); + if (iterator.hasNext()) { + writer.append(String.format("%n")); + } } return writer.toString(); } @@ -111,16 +116,14 @@ public static class AsciidoctorCodeBlockMatcher> extends AsciidoctorCodeBlockMatcher> { - private int headerOffset = 4; + private int headerOffset = 3; protected HttpMatcher() { super("http"); @@ -164,7 +167,6 @@ public HttpRequestMatcher(RequestMethod requestMethod, String uri) { public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String... columns) { - this.addLine(""); this.addLine("|==="); String header = "|" + StringUtils @@ -172,14 +174,13 @@ private AsciidoctorTableMatcher(String... columns) { this.addLine(header); this.addLine(""); this.addLine("|==="); - this.addLine(""); } public AsciidoctorTableMatcher row(String... entries) { for (String entry : entries) { - this.addLine(-2, "|" + entry); + this.addLine(-1, "|" + entry); } - this.addLine(-2, ""); + this.addLine(-1, ""); return this; } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 1d4d46513..39ef530db 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -19,6 +19,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.web.servlet.FlashMap; @@ -76,6 +79,8 @@ private StubMvcResult(RequestBuilder requestBuilder, MockHttpServletResponse res private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; + this.request.setAttribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver())); this.response = response; } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet new file mode 100644 index 000000000..07f3a48ff --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet @@ -0,0 +1 @@ +Custom curl request \ No newline at end of file From 70824aa50948625bf632036c21389c3716e2ce32 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jul 2015 18:01:57 +0100 Subject: [PATCH 0095/1059] Allow documentation of extra attributes on fields, links, and query parameters This commit adds support for associating custom attributes with field, link, and query parameter descriptors. These attributes are then included in the model during snippet rendering. Coupled with a custom snippet template, this enables the inclusion of extra column(s) in the generated tables. Closes gh-70 --- .../docs/asciidoc/customizing-snippets.adoc | 12 ---- .../docs/asciidoc/documenting-your-api.adoc | 63 ++++++++++++++++++- docs/src/docs/asciidoc/index.adoc | 1 - docs/src/test/java/com/example/Payload.java | 15 +++++ .../restdocs/AbstractDescriptor.java | 39 ++++++++++++ .../restdocs/hypermedia/LinkDescriptor.java | 16 ++++- .../hypermedia/LinkSnippetResultHandler.java | 14 ++++- .../restdocs/payload/FieldDescriptor.java | 17 ++++- .../payload/FieldSnippetResultHandler.java | 39 +++++------- .../restdocs/request/ParameterDescriptor.java | 15 ++++- .../QueryParametersSnippetResultHandler.java | 11 +++- .../HypermediaDocumentationTests.java | 30 +++++++++ .../payload/PayloadDocumentationTests.java | 56 +++++++++++++++++ .../request/RequestDocumentationTests.java | 29 +++++++++ .../restdocs/test/StubMvcResult.java | 11 +++- .../links-with-extra-column.snippet | 10 +++ ...query-parameters-with-extra-column.snippet | 10 +++ .../request-fields-with-extra-column.snippet | 11 ++++ .../response-fields-with-extra-column.snippet | 11 ++++ 19 files changed, 364 insertions(+), 46 deletions(-) delete mode 100644 docs/src/docs/asciidoc/customizing-snippets.adoc create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet diff --git a/docs/src/docs/asciidoc/customizing-snippets.adoc b/docs/src/docs/asciidoc/customizing-snippets.adoc deleted file mode 100644 index 94cca79de..000000000 --- a/docs/src/docs/asciidoc/customizing-snippets.adoc +++ /dev/null @@ -1,12 +0,0 @@ -[[customizing-snippets]] -== Customizing the generated snippets - -Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the -generated snippets. You can customize the generated snippets by overriding the -{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[default -templates]. - -Templates are loaded from the classpath in the `org.springframework.restdocs.templates` -package and each template is named after the snippet that it will produce. For example, to -override the template for the `curl-request.adoc` snippet, create a template named -`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index a7652ed40..edac1e2d5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -257,4 +257,65 @@ include::{examples-dir}/com/example/AlwaysDo.java[tags=always-do] With this configuration in place, every call to `MockMvc.perform` will produce the <> without any further configuration. Take a look at the `GettingStartedDocumentation` classes in each of the -sample applications to see this functionality in action. \ No newline at end of file +sample applications to see this functionality in action. + + + +[[documenting-your-api-customizing]] +=== Customizing the output + + + +[[documenting-your-api-customizing-snippets]] +==== Customizing the generated snippets +Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the +generated snippets. +{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +templates] are provided for each of the snippets that Spring REST Docs can produce. To +customize a snippet's content, you can provide your own template. + +Templates are loaded from the classpath in the `org.springframework.restdocs.templates` +package and each template is named after the snippet that it will produce. For example, to +override the template for the `curl-request.adoc` snippet, create a template named +`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. + + + +[[documenting-your-api-customizing-including-extra-information]] +==== Including extra information +The descriptors for fields, links, and query parameters all have an `attribute` method +that can be used to associate one or more key-value pairs with the descriptor. These +attributes are made available during the template rendering process. Coupled with +a custom snippet template, this makes it possible to include extra information in a +generated snippet. + +A concrete example of the above is the addition of a constraints column when documenting +request fields. The first step is to provide a `constraints` attribute for each field that +you are documenting: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=constraints] +---- + +The second step is to provide a custom template named `request-fields.snippet` that +includes the information about the fields' constraints in the generated snippet's table: + +[source,indent=0] +---- + |=== + |Path|Type|Description|Constraints <1> + + {{#fields}} + |{{path}} + |{{type}} + |{{description}} + |{{constraints}} <2> + + {{/fields}} + |=== +---- +<1> Add a new column named "Constraints" +<2> Include the descriptors' `constraints` attribute in each row of the table + + diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index f6afcf0ed..2fb04b36e 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -24,7 +24,6 @@ include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] -include::customizing-snippets.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index bec0f361d..7fcd725a6 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -19,6 +19,7 @@ import static org.springframework.restdocs.RestDocumentation.document; 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.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; @@ -51,4 +52,18 @@ public void explicitType() throws Exception { // end::explicit-type[] } + public void constraints() throws Exception { + this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + // tag::constraints[] + .andDo(document("create-user").withRequestFields( + fieldWithPath("name") + .description("The user's name") + .attribute("constraints", "Must not be null. Must not be empty"), + fieldWithPath("email") + .description("The user's email address") + .attribute("constrains", "Must be a valid email address"))); + // end::constraints[] + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java new file mode 100644 index 000000000..c87637ffa --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java @@ -0,0 +1,39 @@ +package org.springframework.restdocs; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base class for descriptors. Provides the ability to associate arbitrary attributes with + * a descriptor. + * + * @author Andy Wilkinson + * + * @param the type of the descriptor + */ +public abstract class AbstractDescriptor> { + + private final Map attributes = new HashMap<>(); + + /** + * Sets an attribute with the given {@code name} and {@code value}. + * + * @param name The name of the attribute + * @param value The value of the attribute + */ + @SuppressWarnings("unchecked") + public T attribute(String name, Object value) { + this.attributes.put(name, value); + return (T) this; + } + + /** + * Returns the descriptor's attributes + * + * @return the attributes + */ + protected Map getAttributes() { + return this.attributes; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index dca8adcb2..2c1451dc7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.hypermedia; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A description of a link found in a hypermedia API * @@ -23,7 +28,7 @@ * * @author Andy Wilkinson */ -public class LinkDescriptor { +public class LinkDescriptor extends AbstractDescriptor { private final String rel; @@ -67,4 +72,13 @@ String getDescription() { boolean isOptional() { return this.optional; } + + Map toModel() { + Map model = new HashMap<>(); + model.put("rel", this.rel); + model.put("description", this.description); + model.put("optional", this.optional); + model.putAll(getAttributes()); + return model; + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 6d18c6259..06b239dd8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -18,11 +18,13 @@ import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.SnippetGenerationException; @@ -34,7 +36,7 @@ /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * resource's links. - * + * * @author Andy Wilkinson */ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { @@ -114,8 +116,16 @@ private void writeDocumentationSnippet(MvcResult result, PrintWriter writer) TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - context.put("links", this.descriptorsByRel.values()); + context.put("links", createLinksModel()); writer.print(templateEngine.compileTemplate("links").render(context)); } + private List> createLinksModel() { + List> model = new ArrayList<>(); + for (Entry entry : this.descriptorsByRel.entrySet()) { + model.add(entry.getValue().toModel()); + } + return model; + } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 2c81b9eda..be485d625 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.payload; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A description of a field found in a request or response payload * @@ -24,7 +29,7 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class FieldDescriptor { +public class FieldDescriptor extends AbstractDescriptor { private final String path; @@ -86,4 +91,14 @@ boolean isOptional() { String getDescription() { return this.description; } + + Map toModel() { + Map model = new HashMap(); + model.put("path", this.path); + model.put("type", this.type.toString()); + model.put("description", this.description); + model.put("optional", this.optional); + model.putAll(this.getAttributes()); + return model; + } } 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 index 1e87a5519..3ad66c08b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -73,39 +73,32 @@ protected void handle(MvcResult result, PrintWriter writer) throws IOException { Object payload = extractPayload(result); - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - List> fields = new ArrayList<>(); + List> fields = new ArrayList<>(); context.put("fields", fields); for (Entry entry : this.descriptorsByPath.entrySet()) { FieldDescriptor descriptor = entry.getValue(); - FieldType type = getFieldType(descriptor, payload); - Map fieldModel = new HashMap<>(); - fieldModel.put("path", entry.getKey()); - fieldModel.put("type", type.toString()); - fieldModel.put("description", descriptor.getDescription()); - fields.add(fieldModel); + if (descriptor.getType() == null) { + descriptor.type(getFieldType(descriptor, payload)); + } + fields.add(descriptor.toModel()); } + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); writer.print(templateEngine.compileTemplate(this.templateName).render(context)); } private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - if (descriptor.getType() != null) { - return descriptor.getType(); + try { + return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( + descriptor.getPath(), payload); } - else { - try { - return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( - descriptor.getPath(), payload); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the" - + " payload. Please provide a type using" - + " FieldDescriptor.type(FieldType)."; - throw new FieldTypeRequiredException(message); - } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the" + + " payload. Please provide a type using" + + " FieldDescriptor.type(FieldType)."; + throw new FieldTypeRequiredException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 63a1be58b..abc4ee0b0 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.request; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A descriptor of a parameter in a query string * @@ -23,7 +28,7 @@ * @see RequestDocumentation#parameterWithName * */ -public class ParameterDescriptor { +public class ParameterDescriptor extends AbstractDescriptor { private final String name; @@ -52,4 +57,12 @@ String getDescription() { return this.description; } + Map toModel() { + Map model = new HashMap<>(); + model.put("name", this.name); + model.put("description", this.description); + model.putAll(getAttributes()); + return model; + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index 8813d0c8d..661e80ff3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -18,10 +18,13 @@ import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.SnippetGenerationException; @@ -33,7 +36,7 @@ /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting the query * parameters supported by a RESTful resource. - * + * * @author Andy Wilkinson */ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHandler { @@ -90,7 +93,11 @@ private void documentParameters(MvcResult result, PrintWriter writer) TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - context.put("parameters", this.descriptorsByName.values()); + List> parameters = new ArrayList<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + parameters.add(entry.getValue().toModel()); + } + context.put("parameters", parameters); writer.print(templateEngine.compileTemplate("query-parameters").render(context)); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 56588a062..e282f2b93 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -17,6 +17,8 @@ package org.springframework.restdocs.hypermedia; import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; @@ -26,8 +28,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -107,6 +114,29 @@ public void documentedLinks() throws IOException { new LinkDescriptor("b").description("two")).handle(result()); } + @Test + public void linksWithCustomAttributes() throws IOException { + this.snippet.expectLinks("documented-links").withContents( // + tableWithHeader("Relation", "Description", "Foo") // + .row("a", "one", "alpha") // + .row("b", "two", "bravo")); + MockHttpServletRequest request = new MockHttpServletRequest(); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("links")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentLinks( + "documented-links", + new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", + "bravo")), + new LinkDescriptor("a").description("one").attribute("foo", "alpha"), + new LinkDescriptor("b").description("two").attribute("foo", "bravo")) + .handle(result(request)); + } + private static class StubLinkExtractor implements LinkExtractor { private MultiValueMap linksByRel = new LinkedMultiValueMap(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index c96afe0d0..b788da69d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -18,6 +18,8 @@ import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -30,8 +32,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -174,4 +181,53 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException fieldWithPath("a.b").description("one")).handle( result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); } + + @Test + public void requestFieldsWithCustomAttributes() throws IOException { + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); + documentRequestFields("request-fields-with-custom-attributes", + fieldWithPath("a.b").description("one").attribute("foo", "alpha"), + fieldWithPath("a.c").description("two").attribute("foo", "bravo"), + fieldWithPath("a").description("three").attribute("foo", "charlie")) + .handle(result(request)); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"); + documentResponseFields("response-fields-with-custom-attributes", + fieldWithPath("a.b").description("one").attribute("foo", "alpha"), + fieldWithPath("a.c").description("two").attribute("foo", "bravo"), + fieldWithPath("a").description("three").attribute("foo", "charlie")) + .handle(result(request, response)); + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index 32c794b8e..f3a87d6be 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -17,6 +17,8 @@ package org.springframework.restdocs.request; import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; @@ -28,7 +30,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -100,4 +107,26 @@ public void parameterSnippetFromRequestUriQueryString() throws IOException { result(get("/?a=alpha&b=bravo"))); } + @Test + public void parametersWithCustomAttributes() throws IOException { + this.snippet.expectQueryParameters("parameters-with-custom-attributes") + .withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", + "one", "alpha").row("b", "two", "bravo")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("query-parameters")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.addParameter("a", "bravo"); + request.addParameter("b", "bravo"); + documentQueryParameters("parameters-with-custom-attributes", + parameterWithName("a").description("one").attribute("foo", "alpha"), + parameterWithName("b").description("two").attribute("foo", "bravo")) + .handle(result(request)); + + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 39ef530db..7b1d31509 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -61,6 +61,11 @@ public static StubMvcResult result(MockHttpServletResponse response) { return new StubMvcResult(response); } + public static StubMvcResult result(MockHttpServletRequest request, + MockHttpServletResponse response) { + return new StubMvcResult(request, response); + } + private StubMvcResult() { this(new MockHttpServletRequest(), new MockHttpServletResponse()); } @@ -79,8 +84,10 @@ private StubMvcResult(RequestBuilder requestBuilder, MockHttpServletResponse res private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; - this.request.setAttribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + if (this.request.getAttribute(TemplateEngine.class.getName()) == null) { + this.request.setAttribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + } this.response = response; } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet new file mode 100644 index 000000000..81c8232c9 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Relation|Description|Foo + +{{#links}} +|{{rel}} +|{{description}} +|{{foo}} + +{{/links}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet new file mode 100644 index 000000000..fb97f89bf --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Description|Foo + +{{#parameters}} +|{{name}} +|{{description}} +|{{foo}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet new file mode 100644 index 000000000..4830d8956 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet @@ -0,0 +1,11 @@ +|=== +|Path|Type|Description|Foo + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} +|{{foo}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet new file mode 100644 index 000000000..4830d8956 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet @@ -0,0 +1,11 @@ +|=== +|Path|Type|Description|Foo + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} +|{{foo}} + +{{/fields}} +|=== \ No newline at end of file From 02c6c891723f810e5f003074807494cacfc845b8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Jul 2015 15:39:01 +0100 Subject: [PATCH 0096/1059] Allow documentation of extra attributes that apply to the whole snippet This commit adds support for associating custom attributes with the generation of a particular snippet. The attributes are included in the model during snippet rendering allowing them to be referenced from a custom snippet template. Among other things, this makes it possible to provide a configurable title for snippets that produce a code block. Closes gh-77 --- .../docs/asciidoc/documenting-your-api.adoc | 39 ++-- docs/src/test/java/com/example/Payload.java | 12 +- .../restdocs/AbstractDescriptor.java | 16 +- .../springframework/restdocs/Attributes.java | 105 +++++++++++ .../RestDocumentationResultHandler.java | 175 +++++++++++++++++- .../restdocs/curl/CurlDocumentation.java | 14 +- .../restdocs/http/HttpDocumentation.java | 29 ++- .../hypermedia/HypermediaDocumentation.java | 7 +- .../hypermedia/LinkSnippetResultHandler.java | 9 +- .../payload/FieldSnippetResultHandler.java | 5 +- .../payload/PayloadDocumentation.java | 12 +- .../RequestFieldSnippetResultHandler.java | 6 +- .../ResponseFieldSnippetResultHandler.java | 6 +- .../QueryParametersSnippetResultHandler.java | 5 +- .../request/RequestDocumentation.java | 8 +- .../snippet/SnippetWritingResultHandler.java | 18 +- .../restdocs/curl/CurlDocumentationTests.java | 74 +++++--- .../restdocs/http/HttpDocumentationTests.java | 76 ++++++-- .../HypermediaDocumentationTests.java | 48 +++-- .../payload/PayloadDocumentationTests.java | 103 ++++++++--- .../request/RequestDocumentationTests.java | 52 ++++-- .../curl-request-with-title.snippet | 5 + .../http-request-with-title.snippet | 9 + .../http-response-with-title.snippet | 9 + .../links-with-title.snippet | 10 + .../query-parameters-with-title.snippet | 10 + .../request-fields-with-title.snippet | 11 ++ .../response-fields-with-title.snippet | 11 ++ 28 files changed, 726 insertions(+), 158 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index edac1e2d5..3915fecbe 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -283,39 +283,52 @@ override the template for the `curl-request.adoc` snippet, create a template nam [[documenting-your-api-customizing-including-extra-information]] ==== Including extra information -The descriptors for fields, links, and query parameters all have an `attribute` method -that can be used to associate one or more key-value pairs with the descriptor. These -attributes are made available during the template rendering process. Coupled with -a custom snippet template, this makes it possible to include extra information in a -generated snippet. -A concrete example of the above is the addition of a constraints column when documenting -request fields. The first step is to provide a `constraints` attribute for each field that -you are documenting: +There are two ways to provide extra information for inclusion in a generated snippet: + +. Use the `attributes` method on a field, link or query parameter descriptor to add one or + more attributes to an individual descriptor +. Pass in some attributes when calling `withCurlRequest`, `withHttpRequest`, + `withHttpResponse`, etc on `RestDocumentationResultHandler`. Such attributes will be + associated with the snippet as a whole. + +Any additional attributes are made available during the template rendering process. +Coupled with a custom snippet template, this makes it possible to include extra +information in a generated snippet. + +A concrete example of the above is the addition of a constraints column and a title when +documenting request fields. The first step is to provide a `constraints` attribute for +each field that you are documenting and to provide a `title` attribute: [source,java,indent=0] ---- include::{examples-dir}/com/example/Payload.java[tags=constraints] ---- +<1> Configure the `title` attribute for the request fields snippet +<2> Set the `constraints` attribute for the `name` field +<3> Set the `constraints` attribute for the `email` field The second step is to provide a custom template named `request-fields.snippet` that -includes the information about the fields' constraints in the generated snippet's table: +includes the information about the fields' constraints in the generated snippet's table +and adds a title: [source,indent=0] ---- + .{{title}} <1> |=== - |Path|Type|Description|Constraints <1> + |Path|Type|Description|Constraints <2> {{#fields}} |{{path}} |{{type}} |{{description}} - |{{constraints}} <2> + |{{constraints}} <3> {{/fields}} |=== ---- -<1> Add a new column named "Constraints" -<2> Include the descriptors' `constraints` attribute in each row of the table +<1> Add a title to the table +<2> Add a new column named "Constraints" +<3> Include the descriptors' `constraints` attribute in each row of the table diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 7fcd725a6..04c32b365 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -17,12 +17,16 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; 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.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.Attributes.key; import org.springframework.http.MediaType; +import org.springframework.restdocs.Attributes; import org.springframework.restdocs.payload.FieldType; import org.springframework.test.web.servlet.MockMvc; @@ -57,12 +61,16 @@ public void constraints() throws Exception { .andExpect(status().isOk()) // tag::constraints[] .andDo(document("create-user").withRequestFields( + attributes( + key("title").value("Fields for user creation")), // <1> fieldWithPath("name") .description("The user's name") - .attribute("constraints", "Must not be null. Must not be empty"), + .attributes( + key("constraints").value("Must not be null. Must not be empty")), // <2> fieldWithPath("email") .description("The user's email address") - .attribute("constrains", "Must be a valid email address"))); + .attributes( + key("constraints").value("Must be a valid email address")))); // <3> // end::constraints[] } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java index c87637ffa..70150bff6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java @@ -3,6 +3,8 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.restdocs.Attributes.Attribute; + /** * Base class for descriptors. Provides the ability to associate arbitrary attributes with * a descriptor. @@ -13,17 +15,19 @@ */ public abstract class AbstractDescriptor> { - private final Map attributes = new HashMap<>(); + private Map attributes = new HashMap<>(); /** - * Sets an attribute with the given {@code name} and {@code value}. + * Sets the descriptor's attributes * - * @param name The name of the attribute - * @param value The value of the attribute + * @param attributes the attributes + * @return the descriptor */ @SuppressWarnings("unchecked") - public T attribute(String name, Object value) { - this.attributes.put(name, value); + public T attributes(Attribute... attributes) { + for (Attribute attribute : attributes) { + this.attributes.put(attribute.getKey(), attribute.getValue()); + } return (T) this; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java b/spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java new file mode 100644 index 000000000..0ee204abc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java @@ -0,0 +1,105 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Map; + +/** + * A fluent API for building a map of attributes + * + * @author Andy Wilkinson + */ +public abstract class Attributes { + + private Attributes() { + + } + + /** + * Creates an attribute with the given {@code key}. A value for the attribute must + * still be specified. + * + * @param key The key of the attribute + * @return An {@code AttributeBuilder} to use to specify the value of the attribute + * @see AttributeBuilder#value(Object) + */ + public static AttributeBuilder key(String key) { + return new AttributeBuilder(key); + } + + /** + * Creates a {@code Map} of the given {@code attributes}. + * + * @param attributes The attributes + * @return A Map of the attributes + */ + public static Map attributes(Attribute... attributes) { + Map attributeMap = new HashMap<>(); + for (Attribute attribute : attributes) { + attributeMap.put(attribute.getKey(), attribute.getValue()); + } + return attributeMap; + } + + /** + * A simple builder for an attribute (key-value pair) + */ + public static class AttributeBuilder { + + private final String key; + + private AttributeBuilder(String key) { + this.key = key; + } + + /** + * Configures the value of the attribute + * + * @param value The attribute's value + * @return A newly created {@code Attribute} + */ + public Attribute value(Object value) { + return new Attribute(this.key, value); + } + } + + /** + * An attribute (key-value pair). + * + * @author Andy Wilkinson + */ + public static class Attribute { + + private final String key; + + private final Object value; + + public Attribute(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return this.key; + } + + public Object getValue() { + return this.value; + } + + } +} 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 203d3152c..6d26ba633 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.springframework.restdocs.hypermedia.HypermediaDocumentation; import org.springframework.restdocs.hypermedia.LinkDescriptor; @@ -35,6 +36,7 @@ import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.request.ParameterDescriptor; import org.springframework.restdocs.request.RequestDocumentation; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -49,15 +51,55 @@ public class RestDocumentationResultHandler implements ResultHandler { private final String outputDir; - private List delegates = new ArrayList<>(); + private SnippetWritingResultHandler curlRequest; + + private SnippetWritingResultHandler httpRequest; + + private SnippetWritingResultHandler httpResponse; + + private List delegates = new ArrayList<>(); RestDocumentationResultHandler(String outputDir) { this.outputDir = outputDir; + this.curlRequest = documentCurlRequest(this.outputDir, null); + this.httpRequest = documentHttpRequest(this.outputDir, null); + this.httpResponse = documentHttpResponse(this.outputDir, null); + } + + /** + * Customizes the default curl request snippet generation to make the given attributes + * available. + * + * @param attributes the attributes + * @return {@code this} + */ + public RestDocumentationResultHandler withCurlRequest(Map attributes) { + this.curlRequest = documentCurlRequest(this.outputDir, attributes); + return this; + } + + /** + * Customizes the default HTTP request snippet generation to make the given attributes + * available. + * + * @param attributes the attributes + * @return {@code this} + */ + public RestDocumentationResultHandler withHttpRequest(Map attributes) { + this.httpRequest = documentHttpRequest(this.outputDir, attributes); + return this; + } - this.delegates = new ArrayList(); - this.delegates.add(documentCurlRequest(this.outputDir)); - this.delegates.add(documentHttpRequest(this.outputDir)); - this.delegates.add(documentHttpResponse(this.outputDir)); + /** + * Customizes the default HTTP response snippet generation to make the given + * attributes available. + * + * @param attributes the attributes + * @return {@code this} + */ + public RestDocumentationResultHandler withHttpResponse(Map attributes) { + this.httpResponse = documentHttpResponse(this.outputDir, attributes); + return this; } /** @@ -75,7 +117,7 @@ public class RestDocumentationResultHandler implements ResultHandler { * @see LinkExtractors#extractorForContentType(String) */ public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { - return withLinks(null, descriptors); + return withLinks(null, null, descriptors); } /** @@ -94,7 +136,50 @@ public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { */ public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - this.delegates.add(documentLinks(this.outputDir, linkExtractor, descriptors)); + return this.withLinks(null, linkExtractor, descriptors); + } + + /** + * Document the links in the response using the given {@code descriptors}. The links + * are extracted from the response based on its content type. The given + * {@code attributes} are made available during the generation of the links snippet. + *

+ * If a link 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 link is + * described but is not present in the response a failure will also occur when this + * handler is invoked. + * + * @param attributes the attributes + * @param descriptors the link descriptors + * @return {@code this} + * @see HypermediaDocumentation#linkWithRel(String) + * @see LinkExtractors#extractorForContentType(String) + */ + public RestDocumentationResultHandler withLinks(Map attributes, + LinkDescriptor... descriptors) { + return withLinks(attributes, null, descriptors); + } + + /** + * Document the links in the response using the given {@code descriptors}. The links + * are extracted from the response using the given {@code linkExtractor}. The given + * {@code attributes} are made available during the generation of the links snippet. + *

+ * If a link 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 link is + * described but is not present in the response a failure will also occur when this + * handler is invoked. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the link descriptors + * @return {@code this} + * @see HypermediaDocumentation#linkWithRel(String) + */ + public RestDocumentationResultHandler withLinks(Map attributes, + LinkExtractor linkExtractor, LinkDescriptor... descriptors) { + this.delegates.add(documentLinks(this.outputDir, attributes, linkExtractor, + descriptors)); return this; } @@ -114,7 +199,30 @@ public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, */ public RestDocumentationResultHandler withRequestFields( FieldDescriptor... descriptors) { - this.delegates.add(documentRequestFields(this.outputDir, descriptors)); + return this.withRequestFields(null, descriptors); + } + + /** + * Document the fields in the request using the given {@code descriptors}. The given + * {@code attributes} are made available during the generation of the request fields + * snippet. + *

+ * If a field is present in the request but is not documented by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request a + * failure will also occur. For payloads with a hierarchical structure, documenting a + * field is sufficient for all of its descendants to also be treated as having been + * documented. + * + * @param descriptors the link descriptors + * @param attributes the attributes + * @return {@code this} + * @see PayloadDocumentation#fieldWithPath(String) + */ + public RestDocumentationResultHandler withRequestFields( + Map attributes, FieldDescriptor... descriptors) { + this.delegates + .add(documentRequestFields(this.outputDir, attributes, descriptors)); return this; } @@ -134,7 +242,30 @@ public RestDocumentationResultHandler withRequestFields( */ public RestDocumentationResultHandler withResponseFields( FieldDescriptor... descriptors) { - this.delegates.add(documentResponseFields(this.outputDir, descriptors)); + return this.withResponseFields(null, descriptors); + } + + /** + * Document the fields in the response using the given {@code descriptors}. The given + * {@code attributes} are made available during the generation of the request fields + * snippet. + *

+ * If a field is present in the response but is not documented by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + * + * @param descriptors the link descriptors + * @param attributes the attributes + * @return {@code this} + * @see PayloadDocumentation#fieldWithPath(String) + */ + public RestDocumentationResultHandler withResponseFields( + Map attributes, FieldDescriptor... descriptors) { + this.delegates + .add(documentResponseFields(this.outputDir, attributes, descriptors)); return this; } @@ -153,12 +284,36 @@ public RestDocumentationResultHandler withResponseFields( */ public RestDocumentationResultHandler withQueryParameters( ParameterDescriptor... descriptors) { - this.delegates.add(documentQueryParameters(this.outputDir, descriptors)); + return this.withQueryParameters(null, descriptors); + } + + /** + * Documents the parameters in the request's query string using the given + * {@code descriptors}. The given {@code attributes} are made available during the + * generation of the query parameters snippet. + *

+ * If a parameter is present in the query string but is not described by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * parameter is described but is not present in the request a failure will also occur + * when this handler is invoked. + * + * @param descriptors the parameter descriptors + * @param attributes the attributes + * @return {@code this} + * @see RequestDocumentation#parameterWithName(String) + */ + public RestDocumentationResultHandler withQueryParameters( + Map attributes, ParameterDescriptor... descriptors) { + this.delegates.add(documentQueryParameters(this.outputDir, attributes, + descriptors)); return this; } @Override public void handle(MvcResult result) throws Exception { + this.curlRequest.handle(result); + this.httpRequest.handle(result); + this.httpResponse.handle(result); for (ResultHandler delegate : this.delegates) { delegate.handle(result); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index e3161c521..de7ac5316 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -50,10 +50,13 @@ private CurlDocumentation() { * Produces a documentation snippet containing the request formatted as a cURL command * * @param outputDir The directory to which snippet should be written + * @param attributes Attributes made available during rendering of the curl request + * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentCurlRequest(String outputDir) { - return new CurlRequestWritingResultHandler(outputDir); + public static SnippetWritingResultHandler documentCurlRequest(String outputDir, + Map attributes) { + return new CurlRequestWritingResultHandler(outputDir, attributes); } private static final class CurlRequestWritingResultHandler extends @@ -67,15 +70,16 @@ private static final class CurlRequestWritingResultHandler extends private static final int STANDARD_PORT_HTTPS = 443; - private CurlRequestWritingResultHandler(String outputDir) { - super(outputDir, "curl-request"); + private CurlRequestWritingResultHandler(String outputDir, + Map attributes) { + super(outputDir, "curl-request", attributes); } @Override public void handle(MvcResult result, PrintWriter writer) throws IOException { Map context = new HashMap(); - context.put("language", "bash"); context.put("arguments", getCurlCommandArguments(result)); + context.putAll(getAttributes()); TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 2f7448fdf..44690f816 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -54,10 +54,13 @@ private HttpDocumentation() { * request * * @param outputDir The directory to which snippet should be written + * @param attributes Attributes made available during rendering of the HTTP requst + * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpRequest(String outputDir) { - return new HttpRequestWritingResultHandler(outputDir); + public static SnippetWritingResultHandler documentHttpRequest(String outputDir, + Map attributes) { + return new HttpRequestWritingResultHandler(outputDir, attributes); } /** @@ -65,18 +68,22 @@ public static SnippetWritingResultHandler documentHttpRequest(String outputDir) * response sent by the server * * @param outputDir The directory to which snippet should be written + * @param attributes Attributes made available during rendering of the HTTP response + * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpResponse(String outputDir) { - return new HttpResponseWritingResultHandler(outputDir); + public static SnippetWritingResultHandler documentHttpResponse(String outputDir, + Map attributes) { + return new HttpResponseWritingResultHandler(outputDir, attributes); } private static final class HttpRequestWritingResultHandler extends SnippetWritingResultHandler { - private HttpRequestWritingResultHandler(String outputDir) { - super(outputDir, "http-request"); + private HttpRequestWritingResultHandler(String outputDir, + Map attributes) { + super(outputDir, "http-request", attributes); } @Override @@ -84,11 +91,11 @@ public void handle(MvcResult result, PrintWriter writer) throws IOException { DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( result.getRequest()); Map context = new HashMap(); - context.put("language", "http"); context.put("method", result.getRequest().getMethod()); context.put("path", request.getRequestUriWithQueryString()); context.put("headers", getHeaders(request)); context.put("requestBody", getRequestBody(request)); + context.putAll(getAttributes()); TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); @@ -212,8 +219,9 @@ private Map header(String name, String value) { private static final class HttpResponseWritingResultHandler extends SnippetWritingResultHandler { - private HttpResponseWritingResultHandler(String outputDir) { - super(outputDir, "http-response"); + private HttpResponseWritingResultHandler(String outputDir, + Map attributes) { + super(outputDir, "http-response", attributes); } @Override @@ -225,7 +233,6 @@ public void handle(MvcResult result, PrintWriter writer) throws IOException { StringUtils.hasLength(result.getResponse().getContentAsString()) ? String .format("%n%s", result.getResponse().getContentAsString()) : ""); - context.put("language", "http"); context.put("statusCode", status.value()); context.put("statusReason", status.getReasonPhrase()); @@ -238,6 +245,8 @@ public void handle(MvcResult result, PrintWriter writer) throws IOException { } } + context.putAll(getAttributes()); + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 2393fa628..9f9199749 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.hypermedia; import java.util.Arrays; +import java.util.Map; import org.springframework.restdocs.RestDocumentationResultHandler; @@ -48,6 +49,7 @@ public static LinkDescriptor linkWithRel(String rel) { * snippet for a response's links. * * @param outputDir The directory to which the snippet should be written + * @param attributes Attributes made available during rendering of the links snippet * @param linkExtractor Used to extract the links from the response * @param descriptors The descriptions of the response's links * @return the handler @@ -55,8 +57,9 @@ public static LinkDescriptor linkWithRel(String rel) { * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) */ public static LinkSnippetResultHandler documentLinks(String outputDir, - LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinkSnippetResultHandler(outputDir, linkExtractor, + Map attributes, LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + return new LinkSnippetResultHandler(outputDir, attributes, linkExtractor, Arrays.asList(descriptors)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 06b239dd8..ecc78dbb1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -43,13 +43,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { private final Map descriptorsByRel = new LinkedHashMap<>(); - private final Set requiredRels = new HashSet(); + private final Set requiredRels = new HashSet<>(); private final LinkExtractor extractor; - LinkSnippetResultHandler(String outputDir, LinkExtractor linkExtractor, - List descriptors) { - super(outputDir, "links"); + LinkSnippetResultHandler(String outputDir, Map attributes, + LinkExtractor linkExtractor, List descriptors) { + super(outputDir, "links", attributes); this.extractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getRel()); @@ -117,6 +117,7 @@ private void writeDocumentationSnippet(MvcResult result, PrintWriter writer) .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); context.put("links", createLinksModel()); + context.putAll(getAttributes()); writer.print(templateEngine.compileTemplate("links").render(context)); } 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 index 3ad66c08b..bb08851b2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -55,8 +55,8 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private List fieldDescriptors; FieldSnippetResultHandler(String outputDir, String type, - List descriptors) { - super(outputDir, type + "-fields"); + Map attributes, List descriptors) { + super(outputDir, type + "-fields", attributes); this.templateName = type + "-fields"; for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); @@ -83,6 +83,7 @@ protected void handle(MvcResult result, PrintWriter writer) throws IOException { } fields.add(descriptor.toModel()); } + context.putAll(getAttributes()); TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); writer.print(templateEngine.compileTemplate(this.templateName).render(context)); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 2b39a1b25..e49a59726 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.util.Arrays; +import java.util.Map; import org.springframework.restdocs.RestDocumentationResultHandler; @@ -107,14 +108,16 @@ public static FieldDescriptor fieldWithPath(String path) { * documented. * * @param outputDir The directory to which the snippet should be written + * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the request's fields * @return the handler * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) * @see #fieldWithPath(String) */ public static FieldSnippetResultHandler documentRequestFields(String outputDir, - FieldDescriptor... descriptors) { - return new RequestFieldSnippetResultHandler(outputDir, Arrays.asList(descriptors)); + Map attributes, FieldDescriptor... descriptors) { + return new RequestFieldSnippetResultHandler(outputDir, attributes, + Arrays.asList(descriptors)); } /** @@ -129,13 +132,14 @@ public static FieldSnippetResultHandler documentRequestFields(String outputDir, * documented. * * @param outputDir The directory to which the snippet should be written + * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the response's fields * @return the handler * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) */ public static FieldSnippetResultHandler documentResponseFields(String outputDir, - FieldDescriptor... descriptors) { - return new ResponseFieldSnippetResultHandler(outputDir, + Map attributes, FieldDescriptor... descriptors) { + return new ResponseFieldSnippetResultHandler(outputDir, attributes, 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 index b589ef8b4..aad0f6e06 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.Reader; import java.util.List; +import java.util.Map; import org.springframework.test.web.servlet.MvcResult; @@ -28,8 +29,9 @@ */ public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler { - RequestFieldSnippetResultHandler(String outputDir, List descriptors) { - super(outputDir, "request", descriptors); + RequestFieldSnippetResultHandler(String outputDir, Map attributes, + List descriptors) { + super(outputDir, "request", attributes, descriptors); } @Override 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 index af8e3d1f0..76ff2c79c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java @@ -19,6 +19,7 @@ import java.io.Reader; import java.io.StringReader; import java.util.List; +import java.util.Map; import org.springframework.test.web.servlet.MvcResult; @@ -29,8 +30,9 @@ */ public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler { - ResponseFieldSnippetResultHandler(String outputDir, List descriptors) { - super(outputDir, "response", descriptors); + ResponseFieldSnippetResultHandler(String outputDir, Map attributes, + List descriptors) { + super(outputDir, "response", attributes, descriptors); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index 661e80ff3..bbbb3e70e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -44,8 +44,8 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan private final Map descriptorsByName = new LinkedHashMap<>(); protected QueryParametersSnippetResultHandler(String outputDir, - ParameterDescriptor... descriptors) { - super(outputDir, "query-parameters"); + Map attributes, ParameterDescriptor... descriptors) { + super(outputDir, "query-parameters", attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getName()); Assert.hasText(descriptor.getDescription()); @@ -98,6 +98,7 @@ private void documentParameters(MvcResult result, PrintWriter writer) parameters.add(entry.getValue().toModel()); } context.put("parameters", parameters); + context.putAll(getAttributes()); writer.print(templateEngine.compileTemplate("query-parameters").render(context)); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index cef76d164..6fc061c79 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.request; +import java.util.Map; + import org.springframework.restdocs.RestDocumentationResultHandler; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; @@ -35,13 +37,15 @@ private RequestDocumentation() { * documenting a request's query parameters * * @param outputDir The directory to which the snippet should be written + * @param attributes Attributes made available during rendering of the query + * parameters snippet * @param descriptors The descriptions of the parameters in the request's query string * @return the result handler * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) */ public static SnippetWritingResultHandler documentQueryParameters(String outputDir, - ParameterDescriptor... descriptors) { - return new QueryParametersSnippetResultHandler(outputDir, descriptors); + Map attributes, ParameterDescriptor... descriptors) { + return new QueryParametersSnippetResultHandler(outputDir, attributes, descriptors); } /** diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 1a1dc0220..b81594a5d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -23,6 +23,8 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; +import java.util.HashMap; +import java.util.Map; import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.test.web.servlet.MvcResult; @@ -35,13 +37,19 @@ */ public abstract class SnippetWritingResultHandler implements ResultHandler { - private String outputDir; + private final Map attributes = new HashMap<>(); - private String fileName; + private final String outputDir; - protected SnippetWritingResultHandler(String outputDir, String fileName) { + private final String fileName; + + protected SnippetWritingResultHandler(String outputDir, String fileName, + Map attributes) { this.outputDir = outputDir; this.fileName = fileName; + if (attributes != null) { + this.attributes.putAll(attributes); + } } protected abstract void handle(MvcResult result, PrintWriter writer) @@ -54,6 +62,10 @@ public void handle(MvcResult result) throws IOException { } } + protected Map getAttributes() { + return this.attributes; + } + private Writer createWriter() throws IOException { File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName + ".adoc"); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 3a359ef21..74f70e446 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.curl; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.StubMvcResult.result; @@ -29,9 +34,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -54,14 +63,14 @@ public class CurlDocumentationTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - documentCurlRequest("get-request").handle(result(get("/foo"))); + documentCurlRequest("get-request", null).handle(result(get("/foo"))); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - documentCurlRequest("non-get-request").handle(result(post("/foo"))); + documentCurlRequest("non-get-request", null).handle(result(post("/foo"))); } @Test @@ -69,7 +78,7 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - documentCurlRequest("request-with-content").handle( + documentCurlRequest("request-with-content", null).handle( result(get("/foo").content("content"))); } @@ -79,7 +88,7 @@ public void requestWithQueryString() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); - documentCurlRequest("request-with-query-string").handle( + documentCurlRequest("request-with-query-string", null).handle( result(get("/foo?param=value"))); } @@ -87,7 +96,7 @@ public void requestWithQueryString() throws IOException { public void requestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("request-with-one-parameter").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); - documentCurlRequest("request-with-one-parameter").handle( + documentCurlRequest("request-with-one-parameter", null).handle( result(get("/foo").param("k1", "v1"))); } @@ -96,7 +105,7 @@ public void requestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); - documentCurlRequest("request-with-multiple-parameters").handle( + documentCurlRequest("request-with-multiple-parameters", null).handle( result(get("/foo").param("k1", "v1").param("k2", "v2") .param("k1", "v1-bis"))); } @@ -107,7 +116,7 @@ public void requestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); - documentCurlRequest("request-with-url-encoded-parameter").handle( + documentCurlRequest("request-with-url-encoded-parameter", null).handle( result(get("/foo").param("k1", "foo bar&"))); } @@ -116,7 +125,7 @@ public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); - documentCurlRequest("post-request-with-one-parameter").handle( + documentCurlRequest("post-request-with-one-parameter", null).handle( result(post("/foo").param("k1", "v1"))); } @@ -127,7 +136,7 @@ public void postRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - documentCurlRequest("post-request-with-multiple-parameters").handle( + documentCurlRequest("post-request-with-multiple-parameters", null).handle( result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @@ -138,7 +147,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - documentCurlRequest("post-request-with-url-encoded-parameter").handle( + documentCurlRequest("post-request-with-url-encoded-parameter", null).handle( result(post("/foo").param("k1", "a&b"))); } @@ -147,7 +156,7 @@ public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - documentCurlRequest("put-request-with-one-parameter").handle( + documentCurlRequest("put-request-with-one-parameter", null).handle( result(put("/foo").param("k1", "v1"))); } @@ -158,7 +167,7 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - documentCurlRequest("put-request-with-multiple-parameters").handle( + documentCurlRequest("put-request-with-multiple-parameters", null).handle( result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @@ -168,7 +177,7 @@ public void putRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - documentCurlRequest("put-request-with-url-encoded-parameter").handle( + documentCurlRequest("put-request-with-url-encoded-parameter", null).handle( result(put("/foo").param("k1", "a&b"))); } @@ -178,7 +187,7 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - documentCurlRequest("request-with-headers").handle( + documentCurlRequest("request-with-headers", null).handle( result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", "alpha"))); } @@ -189,7 +198,7 @@ public void httpWithNonStandardPort() throws IOException { codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8080); - documentCurlRequest("http-with-non-standard-port").handle(result(request)); + documentCurlRequest("http-with-non-standard-port", null).handle(result(request)); } @Test @@ -199,7 +208,7 @@ public void httpsWithStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(443); request.setScheme("https"); - documentCurlRequest("https-with-standard-port").handle(result(request)); + documentCurlRequest("https-with-standard-port", null).handle(result(request)); } @Test @@ -209,7 +218,7 @@ public void httpsWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8443); request.setScheme("https"); - documentCurlRequest("https-with-non-standard-port").handle(result(request)); + documentCurlRequest("https-with-non-standard-port", null).handle(result(request)); } @Test @@ -218,7 +227,7 @@ public void requestWithCustomHost() throws IOException { codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentCurlRequest("request-with-custom-host").handle(result(request)); + documentCurlRequest("request-with-custom-host", null).handle(result(request)); } @Test @@ -230,7 +239,7 @@ public void requestWithContextPathWithSlash() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("/v3"); - documentCurlRequest("request-with-custom-context-with-slash").handle( + documentCurlRequest("request-with-custom-context-with-slash", null).handle( result(request)); } @@ -243,7 +252,7 @@ public void requestWithContextPathWithoutSlash() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("v3"); - documentCurlRequest("request-with-custom-context-without-slash").handle( + documentCurlRequest("request-with-custom-context-without-slash", null).handle( result(request)); } @@ -256,7 +265,7 @@ public void multipartPostWithNoOriginalFilename() throws IOException { .withContents(codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("metadata", "{\"description\": \"foo\"}".getBytes()); - documentCurlRequest("multipart-post-no-original-filename").handle( + documentCurlRequest("multipart-post-no-original-filename", null).handle( result(fileUpload("/upload").file(multipartFile))); } @@ -270,7 +279,7 @@ public void multipartPostWithContentType() throws IOException { MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "bytes".getBytes()); - documentCurlRequest("multipart-post-with-content-type").handle( + documentCurlRequest("multipart-post-with-content-type", null).handle( result(fileUpload("/upload").file(multipartFile))); } @@ -283,7 +292,7 @@ public void multipartPost() throws IOException { codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "bytes".getBytes()); - documentCurlRequest("multipart-post").handle( + documentCurlRequest("multipart-post", null).handle( result(fileUpload("/upload").file(multipartFile))); } @@ -297,9 +306,26 @@ public void multipartPostWithParameters() throws IOException { codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "bytes".getBytes()); - documentCurlRequest("multipart-post").handle( + documentCurlRequest("multipart-post", null).handle( result(fileUpload("/upload").file(multipartFile) .param("a", "apple", "avocado").param("b", "banana"))); } + @Test + public void customAttributes() throws IOException { + this.snippet.expectCurlRequest("custom-attributes").withContents( + containsString("curl request title")); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("curl-request")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentCurlRequest("custom-attributes", + attributes(key("title").value("curl request title"))).handle( + result(request)); + } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index b2486e325..c10612849 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -16,8 +16,13 @@ package org.springframework.restdocs.http; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.OK; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; @@ -35,11 +40,15 @@ import org.junit.Rule; import org.junit.Test; +import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; /** @@ -61,7 +70,7 @@ public void getRequest() throws IOException { httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( "Alpha", "a")); - documentHttpRequest("get-request").handle( + documentHttpRequest("get-request", null).handle( result(get("/foo").header("Alpha", "a"))); } @@ -70,7 +79,7 @@ public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string").withContents( httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); - documentHttpRequest("get-request-with-query-string").handle( + documentHttpRequest("get-request-with-query-string", null).handle( result(get("/foo?bar=baz"))); } @@ -79,7 +88,7 @@ public void getRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("get-request-with-parameter").withContents( httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost")); - documentHttpRequest("get-request-with-parameter").handle( + documentHttpRequest("get-request-with-parameter", null).handle( result(get("/foo").param("b&r", "baz"))); } @@ -89,7 +98,7 @@ public void postRequestWithContent() throws IOException { httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - documentHttpRequest("post-request-with-content").handle( + documentHttpRequest("post-request-with-content", null).handle( result(post("/foo").content("Hello, world"))); } @@ -100,7 +109,7 @@ public void postRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - documentHttpRequest("post-request-with-parameter").handle( + documentHttpRequest("post-request-with-parameter", null).handle( result(post("/foo").param("b&r", "baz").param("a", "alpha"))); } @@ -110,7 +119,7 @@ public void putRequestWithContent() throws IOException { httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - documentHttpRequest("put-request-with-content").handle( + documentHttpRequest("put-request-with-content", null).handle( result(put("/foo").content("Hello, world"))); } @@ -121,14 +130,14 @@ public void putRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - documentHttpRequest("put-request-with-parameter").handle( + documentHttpRequest("put-request-with-parameter", null).handle( result(put("/foo").param("b&r", "baz").param("a", "alpha"))); } @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); - documentHttpResponse("basic-response").handle(result()); + documentHttpResponse("basic-response", null).handle(result()); } @Test @@ -138,7 +147,7 @@ public void nonOkResponse() throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); response.setStatus(BAD_REQUEST.value()); - documentHttpResponse("non-ok-response").handle(result(response)); + documentHttpResponse("non-ok-response", null).handle(result(response)); } @Test @@ -151,7 +160,7 @@ public void responseWithHeaders() throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setHeader("a", "alpha"); - documentHttpResponse("response-with-headers").handle(result(response)); + documentHttpResponse("response-with-headers", null).handle(result(response)); } @Test @@ -160,7 +169,7 @@ public void responseWithContent() throws IOException { httpResponse(OK).content("content")); MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().append("content"); - documentHttpResponse("response-with-content").handle(result(response)); + documentHttpResponse("response-with-content", null).handle(result(response)); } @Test @@ -175,7 +184,7 @@ public void multipartPost() throws IOException { .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); - documentHttpRequest("multipart-post").handle( + documentHttpRequest("multipart-post", null).handle( result(fileUpload("/upload").file(multipartFile))); } @@ -198,7 +207,7 @@ public void multipartPostWithParameters() throws IOException { .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); - documentHttpRequest("multipart-post").handle( + documentHttpRequest("multipart-post", null).handle( result(fileUpload("/upload").file(multipartFile) .param("a", "apple", "avocado").param("b", "banana"))); } @@ -217,7 +226,7 @@ public void multipartPostWithContentType() throws IOException { MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "<< data >>".getBytes()); - documentHttpRequest("multipart-post-with-content-type").handle( + documentHttpRequest("multipart-post-with-content-type", null).handle( result(fileUpload("/upload").file(multipartFile))); } @@ -229,7 +238,8 @@ public void getRequestWithCustomServerName() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentHttpRequest("get-request-custom-server-name").handle(result(request)); + documentHttpRequest("get-request-custom-server-name", null).handle( + result(request)); } @Test @@ -237,10 +247,44 @@ public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - documentHttpRequest("get-request-custom-host").handle( + documentHttpRequest("get-request-custom-host", null).handle( result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); } + @Test + public void requestWithCustomSnippetAttributes() throws IOException { + this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( + containsString("Title for the request")); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("http-request")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentHttpRequest("request-with-snippet-attributes", + attributes(key("title").value("Title for the request"))).handle( + result(request)); + } + + @Test + public void responseWithCustomSnippetAttributes() throws IOException { + this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( + containsString("Title for the response")); + MockHttpServletRequest request = new MockHttpServletRequest(); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("http-response")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentHttpResponse("response-with-snippet-attributes", + attributes(key("title").value("Title for the response"))).handle( + result(request)); + } + private String createPart(String content) { return this.createPart(content, true); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index e282f2b93..87e3c3df9 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -17,8 +17,11 @@ package org.springframework.restdocs.hypermedia; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; @@ -57,7 +60,7 @@ public void undocumentedLink() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [foo]")); - documentLinks("undocumented-link", + documentLinks("undocumented-link", null, new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle( result()); } @@ -67,7 +70,7 @@ public void missingLink() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - documentLinks("missing-link", new StubLinkExtractor(), + documentLinks("missing-link", null, new StubLinkExtractor(), new LinkDescriptor("foo").description("bar")).handle(result()); } @@ -76,7 +79,7 @@ public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - documentLinks("documented-optional-link", + documentLinks("documented-optional-link", null, new StubLinkExtractor().withLinks(new Link("foo", "blah")), new LinkDescriptor("foo").description("bar").optional()).handle(result()); } @@ -86,7 +89,7 @@ public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - documentLinks("missing-optional-link", new StubLinkExtractor(), + documentLinks("missing-optional-link", null, new StubLinkExtractor(), new LinkDescriptor("foo").description("bar").optional()).handle(result()); } @@ -96,7 +99,7 @@ public void undocumentedLinkAndMissingLink() throws IOException { this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); - documentLinks("undocumented-link-and-missing-link", + documentLinks("undocumented-link-and-missing-link", null, new StubLinkExtractor().withLinks(new Link("a", "alpha")), new LinkDescriptor("foo").description("bar")).handle(result()); } @@ -109,14 +112,15 @@ public void documentedLinks() throws IOException { .row("b", "two")); documentLinks( "documented-links", + null, new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two")).handle(result()); } @Test - public void linksWithCustomAttributes() throws IOException { - this.snippet.expectLinks("documented-links").withContents( // + public void linksWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( // tableWithHeader("Relation", "Description", "Foo") // .row("a", "one", "alpha") // .row("b", "two", "bravo")); @@ -129,12 +133,34 @@ public void linksWithCustomAttributes() throws IOException { request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); documentLinks( - "documented-links", + "links-with-custom-descriptor-attributes", + null, new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), - new LinkDescriptor("a").description("one").attribute("foo", "alpha"), - new LinkDescriptor("b").description("two").attribute("foo", "bravo")) - .handle(result(request)); + new LinkDescriptor("a").description("one").attributes( + key("foo").value("alpha")), + new LinkDescriptor("b").description("two").attributes( + key("foo").value("bravo"))).handle(result(request)); + } + + @Test + public void linksWithCustomAttribute() throws IOException { + this.snippet.expectLinks("links-with-custom-attribute").withContents( + startsWith(".Title for the links")); + MockHttpServletRequest request = new MockHttpServletRequest(); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("links")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-title.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentLinks( + "links-with-custom-attribute", + attributes(key("title").value("Title for the links")), + new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", + "bravo")), new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")).handle(result(request)); } private static class StubLinkExtractor implements LinkExtractor { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index b788da69d..3e3939f81 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -62,7 +64,7 @@ public void mapRequestWithFields() throws IOException { .row("a.c", "String", "two") // .row("a", "Object", "three")); - documentRequestFields("map-request-with-fields", + documentRequestFields("map-request-with-fields", null, fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three")).handle( @@ -77,7 +79,7 @@ public void arrayRequestWithFields() throws IOException { .row("[]a.c", "String", "two") // .row("[]a", "Object", "three")); - documentRequestFields("array-request-with-fields", + documentRequestFields("array-request-with-fields", null, fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three")).handle( @@ -100,7 +102,7 @@ public void mapResponseWithFields() throws IOException { response.getWriter().append( "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}"); - documentResponseFields("map-response-with-fields", + documentResponseFields("map-response-with-fields", null, fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), @@ -121,7 +123,7 @@ public void arrayResponseWithFields() throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter() .append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"); - documentResponseFields("array-response-with-fields", + documentResponseFields("array-response-with-fields", null, fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three")).handle(result(response)); @@ -145,7 +147,7 @@ public void undocumentedRequestField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - documentRequestFields("undocumented-request-fields").handle( + documentRequestFields("undocumented-request-fields", null).handle( result(get("/foo").content("{\"a\": 5}"))); } @@ -155,7 +157,7 @@ public void missingRequestField() throws IOException { this.thrown .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a.b]")); - documentRequestFields("missing-request-fields", + documentRequestFields("missing-request-fields", null, fieldWithPath("a.b").description("one")).handle( result(get("/foo").content("{}"))); } @@ -163,7 +165,7 @@ public void missingRequestField() throws IOException { @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); - documentRequestFields("missing-optional-request-field-with-no-type", + documentRequestFields("missing-optional-request-field-with-no-type", null, fieldWithPath("a.b").description("one").optional()).handle( result(get("/foo").content("{ }"))); } @@ -178,20 +180,20 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a.b]")); documentRequestFields("undocumented-request-field-and-missing-request-field", - fieldWithPath("a.b").description("one")).handle( + null, fieldWithPath("a.b").description("one")).handle( result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); } @Test - public void requestFieldsWithCustomAttributes() throws IOException { - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents( // - tableWithHeader("Path", "Type", "Description", "Foo") // - .row("a.b", "Number", "one", "alpha") // - .row("a.c", "String", "two", "bravo") // - .row("a", "Object", "three", "charlie")); + public void requestFieldsWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestFields( + "request-fields-with-custom-descriptor-attributes").withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("fields")) + when(resolver.resolveTemplateResource("request-fields")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet")); @@ -199,15 +201,19 @@ public void requestFieldsWithCustomAttributes() throws IOException { request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); - documentRequestFields("request-fields-with-custom-attributes", - fieldWithPath("a.b").description("one").attribute("foo", "alpha"), - fieldWithPath("a.c").description("two").attribute("foo", "bravo"), - fieldWithPath("a").description("three").attribute("foo", "charlie")) - .handle(result(request)); + documentRequestFields( + "request-fields-with-custom-descriptor-attributes", + null, + fieldWithPath("a.b").description("one").attributes( + key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes( + key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes( + key("foo").value("charlie"))).handle(result(request)); } @Test - public void responseFieldsWithCustomAttributes() throws IOException { + public void responseFieldsWithCustomDescriptorAttributes() throws IOException { this.snippet.expectResponseFields("response-fields-with-custom-attributes") .withContents( // tableWithHeader("Path", "Type", "Description", "Foo") // @@ -215,7 +221,7 @@ public void responseFieldsWithCustomAttributes() throws IOException { .row("a.c", "String", "two", "bravo") // .row("a", "Object", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("fields")) + when(resolver.resolveTemplateResource("response-fields")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet")); @@ -224,10 +230,53 @@ public void responseFieldsWithCustomAttributes() throws IOException { resolver)); MockHttpServletResponse response = new MockHttpServletResponse(); response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"); + documentResponseFields( + "response-fields-with-custom-attributes", + null, + fieldWithPath("a.b").description("one").attributes( + key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes( + key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes( + key("foo").value("charlie"))).handle(result(request, response)); + } + + @Test + public void requestFieldsWithCustomAttributes() throws IOException { + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("request-fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/request-fields-with-title.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.setContent("{\"a\": \"foo\"}".getBytes()); + MockHttpServletResponse response = new MockHttpServletResponse(); + documentRequestFields("request-fields-with-custom-attributes", + attributes(key("title").value("Custom title")), + fieldWithPath("a").description("one")).handle(result(request, response)); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("response-fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/response-fields-with-title.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getOutputStream().print("{\"a\": \"foo\"}"); documentResponseFields("response-fields-with-custom-attributes", - fieldWithPath("a.b").description("one").attribute("foo", "alpha"), - fieldWithPath("a.c").description("two").attribute("foo", "bravo"), - fieldWithPath("a").description("three").attribute("foo", "charlie")) - .handle(result(request, response)); + attributes(key("title").value("Custom title")), + fieldWithPath("a").description("one")).handle(result(request, response)); } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index f3a87d6be..42ab1f403 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -17,8 +17,11 @@ package org.springframework.restdocs.request; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; @@ -57,7 +60,7 @@ public void undocumentedParameter() throws IOException { this.thrown .expectMessage(equalTo("Query parameters with the following names were" + " not documented: [a]")); - documentQueryParameters("undocumented-parameter").handle( + documentQueryParameters("undocumented-parameter", null).handle( result(get("/").param("a", "alpha"))); } @@ -67,7 +70,7 @@ public void missingParameter() throws IOException { this.thrown .expectMessage(equalTo("Query parameters with the following names were" + " not found in the request: [a]")); - documentQueryParameters("missing-parameter", + documentQueryParameters("missing-parameter", null, parameterWithName("a").description("one")).handle(result(get("/"))); } @@ -78,7 +81,7 @@ public void undocumentedParameterAndMissingParameter() throws IOException { .expectMessage(equalTo("Query parameters with the following names were" + " not documented: [b]. Query parameters with the following" + " names were not found in the request: [a]")); - documentQueryParameters("undocumented-parameter-missing-parameter", + documentQueryParameters("undocumented-parameter-missing-parameter", null, parameterWithName("a").description("one")).handle( result(get("/").param("b", "bravo"))); } @@ -89,7 +92,7 @@ public void parameterSnippetFromRequestParameters() throws IOException { .withContents( tableWithHeader("Parameter", "Description").row("a", "one").row( "b", "two")); - documentQueryParameters("parameter-snippet-request-parameters", + documentQueryParameters("parameter-snippet-request-parameters", null, parameterWithName("a").description("one"), parameterWithName("b").description("two")).handle( result(get("/").param("a", "bravo").param("b", "bravo"))); @@ -101,15 +104,16 @@ public void parameterSnippetFromRequestUriQueryString() throws IOException { .withContents( tableWithHeader("Parameter", "Description").row("a", "one").row( "b", "two")); - documentQueryParameters("parameter-snippet-request-uri-query-string", + documentQueryParameters("parameter-snippet-request-uri-query-string", null, parameterWithName("a").description("one"), parameterWithName("b").description("two")).handle( result(get("/?a=alpha&b=bravo"))); } @Test - public void parametersWithCustomAttributes() throws IOException { - this.snippet.expectQueryParameters("parameters-with-custom-attributes") + public void parametersWithCustomDescriptorAttributes() throws IOException { + this.snippet + .expectQueryParameters("parameters-with-custom-descriptor-attributes") .withContents( tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); @@ -123,10 +127,36 @@ public void parametersWithCustomAttributes() throws IOException { resolver)); request.addParameter("a", "bravo"); request.addParameter("b", "bravo"); - documentQueryParameters("parameters-with-custom-attributes", - parameterWithName("a").description("one").attribute("foo", "alpha"), - parameterWithName("b").description("two").attribute("foo", "bravo")) - .handle(result(request)); + documentQueryParameters( + "parameters-with-custom-descriptor-attributes", + null, + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo"))).handle(result(request)); + } + + @Test + public void parametersWithCustomAttributes() throws IOException { + this.snippet.expectQueryParameters("parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("query-parameters")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.addParameter("a", "bravo"); + request.addParameter("b", "bravo"); + documentQueryParameters( + "parameters-with-custom-attributes", + attributes(key("title").value("The title")), + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo"))).handle(result(request)); } } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet new file mode 100644 index 000000000..b53c203cf --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet @@ -0,0 +1,5 @@ +[source,bash] +.{{title}} +---- +$ curl {{arguments}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet new file mode 100644 index 000000000..812b2e3ff --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet @@ -0,0 +1,9 @@ +[source,http] +.{{title}} +---- +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet new file mode 100644 index 000000000..10931202c --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet @@ -0,0 +1,9 @@ +[source,http] +.{{title}} +---- +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet new file mode 100644 index 000000000..ea4aa06d5 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Relation|Description + +{{#links}} +|{{rel}} +|{{description}} + +{{/links}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet new file mode 100644 index 000000000..611254aa9 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet new file mode 100644 index 000000000..a51b74aee --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet @@ -0,0 +1,11 @@ +.{{title}} +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet new file mode 100644 index 000000000..a51b74aee --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet @@ -0,0 +1,11 @@ +.{{title}} +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file From fcd0492da2e0fa051b32990b2daf9f3126ca4421 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jul 2015 11:10:30 +0100 Subject: [PATCH 0097/1059] Fix compilation error in PayloadDocumentationTests --- .../restdocs/payload/PayloadDocumentationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 3e3939f81..ab4e65f1e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -137,8 +137,8 @@ public void arrayResponse() throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); response.getWriter().append("[\"a\", \"b\", \"c\"]"); - documentResponseFields("array-response", fieldWithPath("[]").description("one")) - .handle(result(response)); + documentResponseFields("array-response", null, + fieldWithPath("[]").description("one")).handle(result(response)); } @Test From e91e8c849010a9846f483768e39c2dcd30abd76b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jul 2015 11:58:04 +0100 Subject: [PATCH 0098/1059] Upgrade to Gradle 2.5 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 51010 -> 52266 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 202910b91..bebb68e92 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.1.RELEASE' + classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2322723c7ed5f591adfa4c87b6c51932f591249a..b5166dad4d90021f6a0b45268c0755719f1d5cd4 100644 GIT binary patch delta 20819 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%0As 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 bASgruN7Vt0FQi$Nz-7#K7@V{Ez1IH$92=ST delta 19525 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 30c1798c7..d94401f1e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:02 BST 2015 +#Tue Jul 28 11:16:06 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip From c3a9bdfa9461ec11e325d1e65838df7ded85c516 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 30 Jun 2015 15:28:22 +0100 Subject: [PATCH 0099/1059] Allow users to control the writer that is used to produce each snippet Previously, SnippetWritingResultHandler hard-coded its own logic for creating the Writer that used to write its snippet. This made it impossible to take complete control over the output location, filename suffix, etc. This commit introduces a new interface, WriterResolver, and a default implementation, StandardWriterResolver. It provides the same functionality as before, but SnippetWritingResultHandler now retrieves an instance of WriterResolver from the request's attributes. This allows users to provide their own WriterResolver implementation. Closes gh-100 --- .../restdocs/ResponseModifier.java | 10 +- .../restdocs/RestDocumentation.java | 8 +- .../RestDocumentationResultHandler.java | 28 ++-- .../config/RestDocumentationConfigurer.java | 49 +++++- .../config/RestDocumentationContext.java | 54 ++---- .../RestDocumentationContextHolder.java | 42 +++++ ...estDocumentationTestExecutionListener.java | 5 +- .../restdocs/config/SnippetConfigurer.java | 8 +- .../restdocs/curl/CurlDocumentation.java | 10 +- .../restdocs/http/HttpDocumentation.java | 20 +-- .../hypermedia/HypermediaDocumentation.java | 6 +- .../hypermedia/LinkSnippetResultHandler.java | 4 +- .../payload/FieldSnippetResultHandler.java | 4 +- .../payload/PayloadDocumentation.java | 12 +- .../RequestFieldSnippetResultHandler.java | 4 +- .../ResponseFieldSnippetResultHandler.java | 4 +- .../QueryParametersSnippetResultHandler.java | 4 +- .../request/RequestDocumentation.java | 7 +- .../restdocs/snippet/OutputFileResolver.java | 101 ------------ ...cumentationContextPlaceholderResolver.java | 89 ++++++++++ .../snippet/SnippetWritingResultHandler.java | 43 +---- .../snippet/StandardWriterResolver.java | 94 +++++++++++ .../restdocs/snippet/WriterResolver.java | 47 ++++++ .../RestDocumentationIntegrationTests.java | 6 + .../RestDocumentationConfigurerTests.java | 46 ++---- .../request/RequestDocumentationTests.java | 9 +- .../snippet/OutputFileResolverTests.java | 155 ------------------ ...tationContextPlaceholderResolverTests.java | 60 +++++++ .../snippet/StandardWriterResolverTests.java | 80 +++++++++ .../RestDocumentationRequestBuilders.java | 18 ++ .../restdocs/test/StubMvcResult.java | 13 +- 31 files changed, 597 insertions(+), 443 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java index f2415a302..e2002904a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java @@ -48,18 +48,18 @@ public final class ResponseModifier { /** * Provides a {@link RestDocumentationResultHandler} that can be used to document the * request and modified result. - * @param outputDir The directory to which the documentation will be written + * @param identifier An identifier for the API call that is being documented * @return the result handler that will produce the documentation */ - public RestDocumentationResultHandler andDocument(String outputDir) { - return new ResponseModifyingRestDocumentationResultHandler(outputDir); + public RestDocumentationResultHandler andDocument(String identifier) { + return new ResponseModifyingRestDocumentationResultHandler(identifier); } class ResponseModifyingRestDocumentationResultHandler extends RestDocumentationResultHandler { - public ResponseModifyingRestDocumentationResultHandler(String outputDir) { - super(outputDir); + public ResponseModifyingRestDocumentationResultHandler(String identifier) { + super(identifier); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index d3a5226dc..58f279d1b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -47,15 +47,15 @@ public static RestDocumentationConfigurer documentationConfiguration() { } /** - * Documents the API call to the given {@code outputDir}. + * Documents the API call using the given {@code identifier}. * - * @param outputDir The directory to which the documentation will be written + * @param identifier An identifier for the API call that is being documented * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) */ - public static RestDocumentationResultHandler document(String outputDir) { - return new RestDocumentationResultHandler(outputDir); + public static RestDocumentationResultHandler document(String identifier) { + return new RestDocumentationResultHandler(identifier); } /** 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 6d26ba633..1d8a04454 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -49,7 +49,7 @@ */ public class RestDocumentationResultHandler implements ResultHandler { - private final String outputDir; + private final String identifier; private SnippetWritingResultHandler curlRequest; @@ -59,11 +59,11 @@ public class RestDocumentationResultHandler implements ResultHandler { private List delegates = new ArrayList<>(); - RestDocumentationResultHandler(String outputDir) { - this.outputDir = outputDir; - this.curlRequest = documentCurlRequest(this.outputDir, null); - this.httpRequest = documentHttpRequest(this.outputDir, null); - this.httpResponse = documentHttpResponse(this.outputDir, null); + RestDocumentationResultHandler(String identifier) { + this.identifier = identifier; + this.curlRequest = documentCurlRequest(this.identifier, null); + this.httpRequest = documentHttpRequest(this.identifier, null); + this.httpResponse = documentHttpResponse(this.identifier, null); } /** @@ -74,7 +74,7 @@ public class RestDocumentationResultHandler implements ResultHandler { * @return {@code this} */ public RestDocumentationResultHandler withCurlRequest(Map attributes) { - this.curlRequest = documentCurlRequest(this.outputDir, attributes); + this.curlRequest = documentCurlRequest(this.identifier, attributes); return this; } @@ -86,7 +86,7 @@ public RestDocumentationResultHandler withCurlRequest(Map attrib * @return {@code this} */ public RestDocumentationResultHandler withHttpRequest(Map attributes) { - this.httpRequest = documentHttpRequest(this.outputDir, attributes); + this.httpRequest = documentHttpRequest(this.identifier, attributes); return this; } @@ -98,7 +98,7 @@ public RestDocumentationResultHandler withHttpRequest(Map attrib * @return {@code this} */ public RestDocumentationResultHandler withHttpResponse(Map attributes) { - this.httpResponse = documentHttpResponse(this.outputDir, attributes); + this.httpResponse = documentHttpResponse(this.identifier, attributes); return this; } @@ -178,7 +178,7 @@ public RestDocumentationResultHandler withLinks(Map attributes, */ public RestDocumentationResultHandler withLinks(Map attributes, LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - this.delegates.add(documentLinks(this.outputDir, attributes, linkExtractor, + this.delegates.add(documentLinks(this.identifier, attributes, linkExtractor, descriptors)); return this; } @@ -222,7 +222,7 @@ public RestDocumentationResultHandler withRequestFields( public RestDocumentationResultHandler withRequestFields( Map attributes, FieldDescriptor... descriptors) { this.delegates - .add(documentRequestFields(this.outputDir, attributes, descriptors)); + .add(documentRequestFields(this.identifier, attributes, descriptors)); return this; } @@ -264,8 +264,8 @@ public RestDocumentationResultHandler withResponseFields( */ public RestDocumentationResultHandler withResponseFields( Map attributes, FieldDescriptor... descriptors) { - this.delegates - .add(documentResponseFields(this.outputDir, attributes, descriptors)); + this.delegates.add(documentResponseFields(this.identifier, attributes, + descriptors)); return this; } @@ -304,7 +304,7 @@ public RestDocumentationResultHandler withQueryParameters( */ public RestDocumentationResultHandler withQueryParameters( Map attributes, ParameterDescriptor... descriptors) { - this.delegates.add(documentQueryParameters(this.outputDir, attributes, + this.delegates.add(documentQueryParameters(this.identifier, attributes, descriptors)); return this; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index c8757d189..0977b68d1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,6 +21,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -49,6 +52,8 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { private TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + private WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); + /** * Creates a new {@link RestDocumentationConfigurer}. * @see RestDocumentation#documentationConfiguration() @@ -56,9 +61,9 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { public RestDocumentationConfigurer() { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( Arrays. asList(this.uriConfigurer, - this.snippetConfigurer, new StepCountConfigurer(), - new ContentLengthHeaderConfigurer(), - new TemplateEngineConfigurer())); + this.writerResolverConfigurer, this.snippetConfigurer, + new StepCountConfigurer(), new ContentLengthHeaderConfigurer(), + this.templateEngineConfigurer)); } public UriConfigurer uris() { @@ -74,6 +79,11 @@ public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) return this; } + public RestDocumentationConfigurer writerResolver(WriterResolver writerResolver) { + this.writerResolverConfigurer.setWriterResolver(writerResolver); + return this; + } + @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { @@ -84,10 +94,10 @@ private static class StepCountConfigurer extends AbstractConfigurer { @Override void apply(MockHttpServletRequest request) { - RestDocumentationContext currentContext = RestDocumentationContext - .currentContext(); - if (currentContext != null) { - currentContext.getAndIncrementStepCount(); + RestDocumentationContext context = (RestDocumentationContext) request + .getAttribute(RestDocumentationContext.class.getName()); + if (context != null) { + context.getAndIncrementStepCount(); } } @@ -122,6 +132,29 @@ void setTemplateEngine(TemplateEngine templateEngine) { } + private static class WriterResolverConfigurer extends AbstractConfigurer { + + private WriterResolver writerResolver; + + @Override + void apply(MockHttpServletRequest request) { + WriterResolver resolverToUse = this.writerResolver; + if (resolverToUse == null) { + resolverToUse = new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolver( + (RestDocumentationContext) request + .getAttribute(RestDocumentationContext.class + .getName()))); + } + request.setAttribute(WriterResolver.class.getName(), resolverToUse); + } + + void setWriterResolver(WriterResolver writerResolver) { + this.writerResolver = writerResolver; + } + + } + private static class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { @@ -134,6 +167,8 @@ private ConfigurerApplyingRequestPostProcessor( @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setAttribute(RestDocumentationContext.class.getName(), + RestDocumentationContextHolder.getCurrentContext()); for (AbstractConfigurer configurer : this.configurers) { configurer.apply(request); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java index 2f48619df..f5b3f2dcf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java @@ -19,42 +19,40 @@ import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.test.context.TestContext; + /** * {@code RestDocumentationContext} encapsulates the context in which the documentation of * a RESTful API is being performed. - * + * * @author Andy Wilkinson */ public final class RestDocumentationContext { - private static final ThreadLocal CONTEXTS = new InheritableThreadLocal(); - private final AtomicInteger stepCount = new AtomicInteger(0); - private final Method testMethod; + private final TestContext testContext; - private String snippetEncoding; - - private RestDocumentationContext() { + public RestDocumentationContext() { this(null); } - private RestDocumentationContext(Method testMethod) { - this.testMethod = testMethod; + public RestDocumentationContext(TestContext testContext) { + this.testContext = testContext; } /** * Returns the test {@link Method method} that is currently executing - * + * * @return The test method */ public Method getTestMethod() { - return this.testMethod; + return this.testContext == null ? null : this.testContext.getTestMethod(); } /** * Gets and then increments the current step count - * + * * @return The step count prior to it being incremented */ int getAndIncrementStepCount() { @@ -63,41 +61,11 @@ int getAndIncrementStepCount() { /** * Gets the current step count - * + * * @return The current step count */ public int getStepCount() { return this.stepCount.get(); } - void setSnippetEncoding(String snippetEncoding) { - this.snippetEncoding = snippetEncoding; - } - - /** - * Gets the encoding to be used when writing snippets - * - * @return The snippet encoding - */ - public String getSnippetEncoding() { - return this.snippetEncoding; - } - - static void establishContext(Method testMethod) { - CONTEXTS.set(new RestDocumentationContext(testMethod)); - } - - static void clearContext() { - CONTEXTS.set(null); - } - - /** - * Returns the current context, never {@code null}. - * - * @return The current context - */ - public static RestDocumentationContext currentContext() { - return CONTEXTS.get(); - } - } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java new file mode 100644 index 000000000..cefb3fbbc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java @@ -0,0 +1,42 @@ +/* + * 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.config; + +import org.springframework.core.NamedInheritableThreadLocal; + +final class RestDocumentationContextHolder { + + private static final NamedInheritableThreadLocal currentContext = new NamedInheritableThreadLocal<>( + "REST Documentation Context"); + + private RestDocumentationContextHolder() { + + } + + static RestDocumentationContext getCurrentContext() { + return currentContext.get(); + } + + static void setCurrentContext(RestDocumentationContext context) { + currentContext.set(context); + } + + static void removeCurrentContext() { + currentContext.remove(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java index b685699b6..7189ad0a1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java @@ -30,11 +30,12 @@ public class RestDocumentationTestExecutionListener extends AbstractTestExecutio @Override public void beforeTestMethod(TestContext testContext) throws Exception { - RestDocumentationContext.establishContext(testContext.getTestMethod()); + RestDocumentationContextHolder.setCurrentContext(new RestDocumentationContext( + testContext)); } @Override public void afterTestMethod(TestContext testContext) throws Exception { - RestDocumentationContext.clearContext(); + RestDocumentationContextHolder.removeCurrentContext(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index e94cb3316..ae992f7fe 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.config; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.snippet.WriterResolver; /** * A configurer that can be used to configure the generated documentation snippets. @@ -52,10 +53,7 @@ public SnippetConfigurer withEncoding(String encoding) { @Override void apply(MockHttpServletRequest request) { - RestDocumentationContext context = RestDocumentationContext.currentContext(); - if (context != null) { - context.setSnippetEncoding(this.snippetEncoding); - } + ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) + .setEncoding(this.snippetEncoding); } - } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index de7ac5316..fbb410fcc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -49,14 +49,14 @@ private CurlDocumentation() { /** * Produces a documentation snippet containing the request formatted as a cURL command * - * @param outputDir The directory to which snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the curl request * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentCurlRequest(String outputDir, + public static SnippetWritingResultHandler documentCurlRequest(String identifier, Map attributes) { - return new CurlRequestWritingResultHandler(outputDir, attributes); + return new CurlRequestWritingResultHandler(identifier, attributes); } private static final class CurlRequestWritingResultHandler extends @@ -70,9 +70,9 @@ private static final class CurlRequestWritingResultHandler extends private static final int STANDARD_PORT_HTTPS = 443; - private CurlRequestWritingResultHandler(String outputDir, + private CurlRequestWritingResultHandler(String identifier, Map attributes) { - super(outputDir, "curl-request", attributes); + super(identifier, "curl-request", attributes); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 44690f816..fd9bdc2f9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -53,37 +53,37 @@ private HttpDocumentation() { * Produces a documentation snippet containing the request formatted as an HTTP * request * - * @param outputDir The directory to which snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the HTTP requst * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpRequest(String outputDir, + public static SnippetWritingResultHandler documentHttpRequest(String identifier, Map attributes) { - return new HttpRequestWritingResultHandler(outputDir, attributes); + return new HttpRequestWritingResultHandler(identifier, attributes); } /** * Produces a documentation snippet containing the response formatted as the HTTP * response sent by the server * - * @param outputDir The directory to which snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the HTTP response * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpResponse(String outputDir, + public static SnippetWritingResultHandler documentHttpResponse(String identifier, Map attributes) { - return new HttpResponseWritingResultHandler(outputDir, attributes); + return new HttpResponseWritingResultHandler(identifier, attributes); } private static final class HttpRequestWritingResultHandler extends SnippetWritingResultHandler { - private HttpRequestWritingResultHandler(String outputDir, + private HttpRequestWritingResultHandler(String identifier, Map attributes) { - super(outputDir, "http-request", attributes); + super(identifier, "http-request", attributes); } @Override @@ -219,9 +219,9 @@ private Map header(String name, String value) { private static final class HttpResponseWritingResultHandler extends SnippetWritingResultHandler { - private HttpResponseWritingResultHandler(String outputDir, + private HttpResponseWritingResultHandler(String identifier, Map attributes) { - super(outputDir, "http-response", attributes); + super(identifier, "http-response", attributes); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 9f9199749..4ea7df09b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -48,7 +48,7 @@ public static LinkDescriptor linkWithRel(String rel) { * Creates a {@code LinkSnippetResultHandler} that will produce a documentation * snippet for a response's links. * - * @param outputDir The directory to which the snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the links snippet * @param linkExtractor Used to extract the links from the response * @param descriptors The descriptions of the response's links @@ -56,10 +56,10 @@ public static LinkDescriptor linkWithRel(String rel) { * @see RestDocumentationResultHandler#withLinks(LinkDescriptor...) * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) */ - public static LinkSnippetResultHandler documentLinks(String outputDir, + public static LinkSnippetResultHandler documentLinks(String identifier, Map attributes, LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinkSnippetResultHandler(outputDir, attributes, linkExtractor, + return new LinkSnippetResultHandler(identifier, attributes, linkExtractor, Arrays.asList(descriptors)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index ecc78dbb1..cf0f98990 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -47,9 +47,9 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { private final LinkExtractor extractor; - LinkSnippetResultHandler(String outputDir, Map attributes, + LinkSnippetResultHandler(String identifier, Map attributes, LinkExtractor linkExtractor, List descriptors) { - super(outputDir, "links", attributes); + super(identifier, "links", attributes); this.extractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getRel()); 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 index bb08851b2..3bc1701f2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -54,9 +54,9 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private List fieldDescriptors; - FieldSnippetResultHandler(String outputDir, String type, + FieldSnippetResultHandler(String identifier, String type, Map attributes, List descriptors) { - super(outputDir, type + "-fields", attributes); + super(identifier, type + "-fields", attributes); this.templateName = type + "-fields"; for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index e49a59726..4e9d34c64 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -107,16 +107,16 @@ public static FieldDescriptor fieldWithPath(String path) { * field is sufficient for all of its descendants to also be treated as having been * documented. * - * @param outputDir The directory to which the snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the request's fields * @return the handler * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) * @see #fieldWithPath(String) */ - public static FieldSnippetResultHandler documentRequestFields(String outputDir, + public static FieldSnippetResultHandler documentRequestFields(String identifier, Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldSnippetResultHandler(outputDir, attributes, + return new RequestFieldSnippetResultHandler(identifier, attributes, Arrays.asList(descriptors)); } @@ -131,15 +131,15 @@ public static FieldSnippetResultHandler documentRequestFields(String outputDir, * field is sufficient for all of its descendants to also be treated as having been * documented. * - * @param outputDir The directory to which the snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the response's fields * @return the handler * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) */ - public static FieldSnippetResultHandler documentResponseFields(String outputDir, + public static FieldSnippetResultHandler documentResponseFields(String identifier, Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldSnippetResultHandler(outputDir, attributes, + return new ResponseFieldSnippetResultHandler(identifier, attributes, 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 index aad0f6e06..851204d2a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java @@ -29,9 +29,9 @@ */ public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler { - RequestFieldSnippetResultHandler(String outputDir, Map attributes, + RequestFieldSnippetResultHandler(String identifier, Map attributes, List descriptors) { - super(outputDir, "request", attributes, descriptors); + super(identifier, "request", attributes, descriptors); } @Override 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 index 76ff2c79c..e5af663c6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java @@ -30,9 +30,9 @@ */ public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler { - ResponseFieldSnippetResultHandler(String outputDir, Map attributes, + ResponseFieldSnippetResultHandler(String identifier, Map attributes, List descriptors) { - super(outputDir, "response", attributes, descriptors); + super(identifier, "response", attributes, descriptors); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index bbbb3e70e..ad45e62b7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -43,9 +43,9 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan private final Map descriptorsByName = new LinkedHashMap<>(); - protected QueryParametersSnippetResultHandler(String outputDir, + protected QueryParametersSnippetResultHandler(String identifier, Map attributes, ParameterDescriptor... descriptors) { - super(outputDir, "query-parameters", attributes); + super(identifier, "query-parameters", attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getName()); Assert.hasText(descriptor.getDescription()); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 6fc061c79..aadcf76ac 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -36,16 +36,17 @@ private RequestDocumentation() { * Creates a {@link SnippetWritingResultHandler} that will produce a snippet * documenting a request's query parameters * - * @param outputDir The directory to which the snippet should be written + * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the query * parameters snippet * @param descriptors The descriptions of the parameters in the request's query string * @return the result handler * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) */ - public static SnippetWritingResultHandler documentQueryParameters(String outputDir, + public static SnippetWritingResultHandler documentQueryParameters(String identifier, Map attributes, ParameterDescriptor... descriptors) { - return new QueryParametersSnippetResultHandler(outputDir, attributes, descriptors); + return new QueryParametersSnippetResultHandler(identifier, attributes, + descriptors); } /** diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java deleted file mode 100644 index 86b9facec..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java +++ /dev/null @@ -1,101 +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.snippet; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.restdocs.config.RestDocumentationContext; - -/** - * {@code OutputFileResolver} resolves an absolute output file based on the current - * configuration and context. - * - * @author Andy Wilkinson - */ -class OutputFileResolver { - - private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])"); - - File resolve(String outputDirectory, String fileName) { - Map replacements = createReplacements(); - String path = outputDirectory; - for (Entry replacement : replacements.entrySet()) { - while (path.contains(replacement.getKey())) { - if (replacement.getValue() == null) { - throw new IllegalStateException("No replacement is available for " - + replacement.getKey()); - } - else { - path = path.replace(replacement.getKey(), replacement.getValue()); - } - } - } - - File outputFile = new File(path, fileName); - if (!outputFile.isAbsolute()) { - outputFile = makeRelativeToConfiguredOutputDir(outputFile); - } - return outputFile; - } - - private Map createReplacements() { - RestDocumentationContext context = RestDocumentationContext.currentContext(); - - Map replacements = new HashMap(); - replacements.put("{methodName}", context == null ? null : context.getTestMethod() - .getName()); - replacements.put("{method-name}", context == null ? null - : camelCaseToDash(context.getTestMethod().getName())); - replacements.put("{method_name}", context == null ? null - : camelCaseToUnderscore(context.getTestMethod().getName())); - replacements.put("{step}", - context == null ? null : Integer.toString(context.getStepCount())); - - return replacements; - } - - private String camelCaseToDash(String string) { - return camelCaseToSeparator(string, "-"); - } - - private String camelCaseToUnderscore(String string) { - return camelCaseToSeparator(string, "_"); - } - - private String camelCaseToSeparator(String string, String separator) { - Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase()); - } - matcher.appendTail(result); - return result.toString(); - } - - private File makeRelativeToConfiguredOutputDir(File outputFile) { - File configuredOutputDir = new DocumentationProperties().getOutputDir(); - if (configuredOutputDir != null) { - return new File(configuredOutputDir, outputFile.getPath()); - } - return null; - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java new file mode 100644 index 000000000..e78b50f63 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -0,0 +1,89 @@ +/* + * 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.snippet; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * A {@link PlaceholderResolver} that resolves placeholders using a + * {@link RestDocumentationContext}. The following placeholders are supported: + *

    + *
  • {@code step} – the {@link RestDocumentationContext#getStepCount() step current + * count}. + *
  • {@code methodName} - the name of the + * {@link RestDocumentationContext#getTestMethod() current test method} formatted using + * camelCase + *
  • {@code method-name} - the name of the + * {@link RestDocumentationContext#getTestMethod() current test method} formatted using + * kebab-case + *
  • {@code method_name} - the name of the + * {@link RestDocumentationContext#getTestMethod() current test method} formatted using + * snake_case + *
+ * + * @author Andy Wilkinson + */ +public class RestDocumentationContextPlaceholderResolver implements PlaceholderResolver { + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])"); + + private final RestDocumentationContext context; + + public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) { + this.context = context; + } + + @Override + public String resolvePlaceholder(String placeholderName) { + if ("step".equals(placeholderName)) { + return Integer.toString(this.context.getStepCount()); + } + if ("methodName".equals(placeholderName)) { + return this.context.getTestMethod().getName(); + } + if ("method-name".equals(placeholderName)) { + return camelCaseToDash(this.context.getTestMethod().getName()); + } + if ("method_name".equals(placeholderName)) { + return camelCaseToUnderscore(this.context.getTestMethod().getName()); + } + return null; + } + + private String camelCaseToDash(String string) { + return camelCaseToSeparator(string, "-"); + } + + private String camelCaseToUnderscore(String string) { + return camelCaseToSeparator(string, "_"); + } + + private String camelCaseToSeparator(String string, String separator) { + Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase()); + } + matcher.appendTail(result); + return result.toString(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index b81594a5d..c75c351ec 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -16,37 +16,32 @@ package org.springframework.restdocs.snippet; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; /** * Base class for a {@link ResultHandler} that writes a documentation snippet - * + * * @author Andy Wilkinson */ public abstract class SnippetWritingResultHandler implements ResultHandler { private final Map attributes = new HashMap<>(); - private final String outputDir; + private final String identifier; - private final String fileName; + private final String snippetName; - protected SnippetWritingResultHandler(String outputDir, String fileName, + protected SnippetWritingResultHandler(String identifier, String snippetName, Map attributes) { - this.outputDir = outputDir; - this.fileName = fileName; + this.identifier = identifier; + this.snippetName = snippetName; if (attributes != null) { this.attributes.putAll(attributes); } @@ -57,7 +52,9 @@ protected abstract void handle(MvcResult result, PrintWriter writer) @Override public void handle(MvcResult result) throws IOException { - try (Writer writer = createWriter()) { + WriterResolver writerResolver = (WriterResolver) result.getRequest() + .getAttribute(WriterResolver.class.getName()); + try (Writer writer = writerResolver.resolve(this.identifier, this.snippetName)) { handle(result, new PrintWriter(writer)); } } @@ -66,26 +63,4 @@ protected Map getAttributes() { return this.attributes; } - private Writer createWriter() throws IOException { - File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName - + ".adoc"); - - if (outputFile != null) { - File parent = outputFile.getParentFile(); - if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IllegalStateException("Failed to create directory '" + parent - + "'"); - } - RestDocumentationContext context = RestDocumentationContext.currentContext(); - if (context == null || context.getSnippetEncoding() == null) { - return new FileWriter(outputFile); - } - return new OutputStreamWriter(new FileOutputStream(outputFile), - context.getSnippetEncoding()); - } - else { - return new OutputStreamWriter(System.out); - } - } - } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java new file mode 100644 index 000000000..fd4a52a79 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -0,0 +1,94 @@ +/* + * 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.snippet; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * Standard implementation of {@link WriterResolver}. + * + * @author Andy Wilkinson + */ +public class StandardWriterResolver implements WriterResolver { + + private String encoding = "UTF-8"; + + private final PlaceholderResolver placeholderResolver; + + private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( + "{", "}"); + + /** + * Creates a new {@code StandardWriterResolver} that will use the given + * {@code placeholderResolver} to resolve any placeholders in the + * {@code operationName}. + * + * @param placeholderResolver the placeholder resolver + */ + public StandardWriterResolver(PlaceholderResolver placeholderResolver) { + this.placeholderResolver = placeholderResolver; + } + + @Override + public Writer resolve(String operationName, String snippetName) throws IOException { + File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( + operationName, this.placeholderResolver), snippetName + ".adoc"); + + if (outputFile != null) { + createDirectoriesIfNecessary(outputFile); + return new OutputStreamWriter(new FileOutputStream(outputFile), this.encoding); + } + else { + return new OutputStreamWriter(System.out, this.encoding); + } + } + + @Override + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + protected File resolveFile(String outputDirectory, String fileName) { + File outputFile = new File(outputDirectory, fileName); + if (!outputFile.isAbsolute()) { + outputFile = makeRelativeToConfiguredOutputDir(outputFile); + } + return outputFile; + } + + private File makeRelativeToConfiguredOutputDir(File outputFile) { + File configuredOutputDir = new DocumentationProperties().getOutputDir(); + if (configuredOutputDir != null) { + return new File(configuredOutputDir, outputFile.getPath()); + } + return null; + } + + private void createDirectoriesIfNecessary(File outputFile) { + File parent = outputFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IllegalStateException("Failed to create directory '" + parent + "'"); + } + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java new file mode 100644 index 000000000..45948fc5a --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -0,0 +1,47 @@ +/* + * 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.snippet; + +import java.io.IOException; +import java.io.Writer; + +/** + * A {@code WriterResolver} is used to access the {@link Writer} that should be used to + * write a snippet for an operation that is being documented. + * + * @author Andy Wilkinson + */ +public interface WriterResolver { + + /** + * Returns a writer that can be used to write the snippet with the given name for the + * operation with the given name. + * @param operationName the name of the operation that is being documented + * @param snippetName the name of the snippet + * @return the writer + * @throws IOException if a writer cannot be resolved + */ + Writer resolve(String operationName, String snippetName) throws IOException; + + /** + * Configures the encoding that should be used by any writers produced by this + * resolver + * @param encoding the encoding + */ + void setEncoding(String encoding); + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index e5aa52a61..95049d786 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.Attributes.attributes; +import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; @@ -199,6 +201,10 @@ public void customSnippetTemplate() throws Exception { assertThat(new File( "build/generated-snippets/custom-snippet-template/curl-request.adoc"), is(snippet().withContents(equalTo("Custom curl request")))); + + mockMvc.perform(get("/")).andDo( + document("index").withCurlRequest( + attributes(key("title").value("Access the index using curl")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 35e95c1d6..1f684b176 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,6 +24,8 @@ import java.net.URI; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; @@ -41,6 +43,18 @@ public class RestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); + private RestDocumentationContext context = new RestDocumentationContext(); + + @Before + public void establishContext() { + RestDocumentationContextHolder.setCurrentContext(this.context); + } + + @After + public void clearContext() { + RestDocumentationContextHolder.removeCurrentContext(); + } + @Test public void defaultConfiguration() { RequestPostProcessor postProcessor = new RestDocumentationConfigurer() @@ -118,38 +132,6 @@ public void contentLengthHeaderIsSetWhenRequestHasContent() { is(equalTo(Integer.toString(content.length)))); } - @Test - public void defaultSnippetEncodingIsAppliedToTheContext() { - RestDocumentationContext.establishContext(null); - try { - assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), - is(nullValue())); - new RestDocumentationConfigurer().beforeMockMvcCreated(null, null) - .postProcessRequest(this.request); - assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), - is(equalTo("UTF-8"))); - } - finally { - RestDocumentationContext.clearContext(); - } - } - - @Test - public void customSnippetEncodingIsAppliedToTheContext() { - RestDocumentationContext.establishContext(null); - try { - assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), - is(nullValue())); - new RestDocumentationConfigurer().snippets().withEncoding("foo") - .beforeMockMvcCreated(null, null).postProcessRequest(this.request); - assertThat(RestDocumentationContext.currentContext().getSnippetEncoding(), - is(equalTo("foo"))); - } - finally { - RestDocumentationContext.clearContext(); - } - } - private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index 42ab1f403..85490138f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -24,9 +24,9 @@ import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.test.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import java.io.IOException; @@ -35,6 +35,7 @@ import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -43,7 +44,7 @@ /** * Requests for {@link RequestDocumentation} - * + * * @author Andy Wilkinson */ public class RequestDocumentationTests { @@ -107,7 +108,9 @@ public void parameterSnippetFromRequestUriQueryString() throws IOException { documentQueryParameters("parameter-snippet-request-uri-query-string", null, parameterWithName("a").description("one"), parameterWithName("b").description("two")).handle( - result(get("/?a=alpha&b=bravo"))); + result(get("/?a=alpha&b=bravo").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext()))); } @Test diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java deleted file mode 100644 index 757552849..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java +++ /dev/null @@ -1,155 +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.snippet; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.lang.reflect.Method; - -import org.junit.Test; -import org.springframework.restdocs.config.RestDocumentationTestExecutionListener; -import org.springframework.test.context.TestContext; - -/** - * Tests for {@link OutputFileResolver}. - * - * @author Andy Wilkinson - */ -public class OutputFileResolverTests { - - private final OutputFileResolver resolver = new OutputFileResolver(); - - @Test - public void noConfiguredOutputDirectoryAndRelativeInput() { - assertThat(this.resolver.resolve("foo", "bar.txt"), is(nullValue())); - } - - @Test - public void absoluteInput() { - String absolutePath = new File("foo").getAbsolutePath(); - assertThat(this.resolver.resolve(absolutePath, "bar.txt"), is(new File( - absolutePath, "bar.txt"))); - } - - @Test - public void configuredOutputAndRelativeInput() { - String outputDir = new File("foo").getAbsolutePath(); - System.setProperty("org.springframework.restdocs.outputDir", outputDir); - try { - assertThat(this.resolver.resolve("bar", "baz.txt"), is(new File(outputDir, - "bar/baz.txt"))); - } - finally { - System.clearProperty("org.springframework.restdocs.outputDir"); - } - } - - @Test - public void configuredOutputAndAbsoluteInput() { - String outputDir = new File("foo").getAbsolutePath(); - String absolutePath = new File("bar").getAbsolutePath(); - System.setProperty("org.springframework.restdocs.outputDir", outputDir); - try { - assertThat(this.resolver.resolve(absolutePath, "baz.txt"), is(new File( - absolutePath, "baz.txt"))); - } - finally { - System.clearProperty("org.springframework.restdocs.outputDir"); - } - } - - @Test(expected = IllegalStateException.class) - public void placeholderWithoutAReplacement() { - this.resolver.resolve("{method-name}", "foo.txt"); - } - - @Test - public void dashSeparatedMethodName() throws Exception { - RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); - TestContext testContext = mock(TestContext.class); - Method method = getClass().getMethod("dashSeparatedMethodName"); - when(testContext.getTestMethod()).thenReturn(method); - listener.beforeTestMethod(testContext); - try { - assertThat(this.resolver.resolve(new File("{method-name}").getAbsolutePath(), - "foo.txt"), - is(new File(new File("dash-separated-method-name").getAbsolutePath(), - "foo.txt"))); - } - finally { - listener.afterTestMethod(testContext); - } - } - - @Test - public void underscoreSeparatedMethodName() throws Exception { - RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); - TestContext testContext = mock(TestContext.class); - Method method = getClass().getMethod("underscoreSeparatedMethodName"); - when(testContext.getTestMethod()).thenReturn(method); - listener.beforeTestMethod(testContext); - try { - assertThat( - this.resolver.resolve(new File("{method_name}").getAbsolutePath(), - "foo.txt"), - is(new File(new File("underscore_separated_method_name") - .getAbsolutePath(), "foo.txt"))); - } - finally { - listener.afterTestMethod(testContext); - } - } - - @Test - public void camelCaseMethodName() throws Exception { - RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); - TestContext testContext = mock(TestContext.class); - Method method = getClass().getMethod("camelCaseMethodName"); - when(testContext.getTestMethod()).thenReturn(method); - listener.beforeTestMethod(testContext); - try { - assertThat(this.resolver.resolve(new File("{methodName}").getAbsolutePath(), - "foo.txt"), - is(new File(new File("camelCaseMethodName").getAbsolutePath(), - "foo.txt"))); - } - finally { - listener.afterTestMethod(testContext); - } - } - - @Test - public void stepCount() throws Exception { - RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); - TestContext testContext = mock(TestContext.class); - Method method = getClass().getMethod("stepCount"); - when(testContext.getTestMethod()).thenReturn(method); - listener.beforeTestMethod(testContext); - try { - assertThat(this.resolver.resolve(new File("{step}").getAbsolutePath(), - "foo.txt"), is(new File(new File("0").getAbsolutePath(), "foo.txt"))); - } - finally { - listener.afterTestMethod(testContext); - } - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java new file mode 100644 index 000000000..468b016b7 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -0,0 +1,60 @@ +package org.springframework.restdocs.snippet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.test.context.TestContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * Tests for {@link RestDocumentationContextPlaceholderResolver} + * + * @author Andy Wilkinson + * + */ +public class RestDocumentationContextPlaceholderResolverTests { + + private final TestContext testContext = mock(TestContext.class); + + private final RestDocumentationContext context = new RestDocumentationContext( + this.testContext); + + private final PlaceholderResolver resolver = new RestDocumentationContextPlaceholderResolver( + this.context); + + @Test + public void dashSeparatedMethodName() throws Exception { + when(this.testContext.getTestMethod()).thenReturn( + getClass().getMethod("dashSeparatedMethodName")); + assertThat(this.resolver.resolvePlaceholder("method-name"), + equalTo("dash-separated-method-name")); + } + + @Test + public void underscoreSeparatedMethodName() throws Exception { + when(this.testContext.getTestMethod()).thenReturn( + getClass().getMethod("underscoreSeparatedMethodName")); + assertThat(this.resolver.resolvePlaceholder("method_name"), + equalTo("underscore_separated_method_name")); + } + + @Test + public void camelCaseMethodName() throws Exception { + Method method = getClass().getMethod("camelCaseMethodName"); + when(this.testContext.getTestMethod()).thenReturn(method); + assertThat(this.resolver.resolvePlaceholder("methodName"), + equalTo("camelCaseMethodName")); + } + + @Test + public void stepCount() throws Exception { + assertThat(this.resolver.resolvePlaceholder("step"), equalTo("0")); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java new file mode 100644 index 000000000..37d8fa444 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -0,0 +1,80 @@ +/* + * 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.snippet; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +import java.io.File; + +import org.junit.Test; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * Tests for {@link StandardWriterResolver}. + * + * @author Andy Wilkinson + */ +public class StandardWriterResolverTests { + + private final PlaceholderResolver placeholderResolver = mock(PlaceholderResolver.class); + + private final StandardWriterResolver resolver = new StandardWriterResolver( + this.placeholderResolver); + + @Test + public void noConfiguredOutputDirectoryAndRelativeInput() { + assertThat(this.resolver.resolveFile("foo", "bar.txt"), is(nullValue())); + } + + @Test + public void absoluteInput() { + String absolutePath = new File("foo").getAbsolutePath(); + assertThat(this.resolver.resolveFile(absolutePath, "bar.txt"), is(new File( + absolutePath, "bar.txt"))); + } + + @Test + public void configuredOutputAndRelativeInput() { + String outputDir = new File("foo").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolveFile("bar", "baz.txt"), is(new File( + outputDir, "bar/baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + + @Test + public void configuredOutputAndAbsoluteInput() { + String outputDir = new File("foo").getAbsolutePath(); + String absolutePath = new File("bar").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolveFile(absolutePath, "baz.txt"), is(new File( + absolutePath, "baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java new file mode 100644 index 000000000..05bcd7b47 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java @@ -0,0 +1,18 @@ +package org.springframework.restdocs.test; + +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +public class RestDocumentationRequestBuilders { + + private RestDocumentationRequestBuilders() { + + } + + public static MockHttpServletRequestBuilder get(String urlTemplate) { + return MockMvcRequestBuilders.get(urlTemplate).requestAttr( + RestDocumentationContext.class.getName(), new RestDocumentationContext()); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 7b1d31509..441398a25 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -19,6 +19,10 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -30,7 +34,7 @@ /** * A minimal stub implementation of {@link MvcResult} - * + * * @author Andy Wilkinson * */ @@ -88,6 +92,13 @@ private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse re this.request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(new StandardTemplateResourceResolver())); } + RestDocumentationContext context = new RestDocumentationContext(); + this.request.setAttribute(RestDocumentationContext.class.getName(), context); + if (this.request.getAttribute(WriterResolver.class.getName()) == null) { + this.request.setAttribute(WriterResolver.class.getName(), + new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolver(context))); + } this.response = response; } From ccc241860abde4da528011abf221d58439da7226 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jul 2015 16:08:38 +0100 Subject: [PATCH 0100/1059] Isolate use of TemplateEngine into existing common base class --- .../restdocs/curl/CurlDocumentation.java | 14 ++---- .../restdocs/http/HttpDocumentation.java | 45 +++++++------------ .../hypermedia/LinkSnippetResultHandler.java | 18 ++------ .../payload/FieldSnippetResultHandler.java | 19 ++------ .../QueryParametersSnippetResultHandler.java | 27 ++++------- .../snippet/SnippetWritingResultHandler.java | 15 +++---- 6 files changed, 45 insertions(+), 93 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index fbb410fcc..9682fec50 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -25,7 +25,6 @@ import java.util.Map.Entry; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -76,15 +75,10 @@ private CurlRequestWritingResultHandler(String identifier, } @Override - public void handle(MvcResult result, PrintWriter writer) throws IOException { - Map context = new HashMap(); - context.put("arguments", getCurlCommandArguments(result)); - context.putAll(getAttributes()); - - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - - writer.print(templateEngine.compileTemplate("curl-request").render(context)); + public Map doHandle(MvcResult result) throws IOException { + Map model = new HashMap(); + model.put("arguments", getCurlCommandArguments(result)); + return model; } private String getCurlCommandArguments(MvcResult result) throws IOException { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index fd9bdc2f9..474f82f28 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -29,7 +29,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -87,20 +86,15 @@ private HttpRequestWritingResultHandler(String identifier, } @Override - public void handle(MvcResult result, PrintWriter writer) throws IOException { + public Map doHandle(MvcResult result) throws IOException { DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( result.getRequest()); - Map context = new HashMap(); - context.put("method", result.getRequest().getMethod()); - context.put("path", request.getRequestUriWithQueryString()); - context.put("headers", getHeaders(request)); - context.put("requestBody", getRequestBody(request)); - context.putAll(getAttributes()); - - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - - writer.print(templateEngine.compileTemplate("http-request").render(context)); + Map model = new HashMap(); + model.put("method", result.getRequest().getMethod()); + model.put("path", request.getRequestUriWithQueryString()); + model.put("headers", getHeaders(request)); + model.put("requestBody", getRequestBody(request)); + return model; } private List> getHeaders( @@ -225,33 +219,28 @@ private HttpResponseWritingResultHandler(String identifier, } @Override - public void handle(MvcResult result, PrintWriter writer) throws IOException { + public Map doHandle(MvcResult result) throws IOException { HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); - Map context = new HashMap(); - context.put( + Map model = new HashMap(); + model.put( "responseBody", StringUtils.hasLength(result.getResponse().getContentAsString()) ? String .format("%n%s", result.getResponse().getContentAsString()) : ""); - context.put("statusCode", status.value()); - context.put("statusReason", status.getReasonPhrase()); + model.put("statusCode", status.value()); + model.put("statusReason", status.getReasonPhrase()); + model.put("headers", headers(result)); + return model; + } + private List> headers(MvcResult result) { List> headers = new ArrayList<>(); - context.put("headers", headers); - for (String headerName : result.getResponse().getHeaderNames()) { for (String header : result.getResponse().getHeaders(headerName)) { headers.add(header(headerName, header)); } } - - context.putAll(getAttributes()); - - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - - writer.print(templateEngine.compileTemplate("http-response").render(context)); - + return headers; } private Map header(String name, String value) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index cf0f98990..68155007f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -17,7 +17,6 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; -import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -29,7 +28,6 @@ import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -62,9 +60,11 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } @Override - protected void handle(MvcResult result, PrintWriter writer) throws IOException { + protected Map doHandle(MvcResult result) throws IOException { validate(extractLinks(result)); - writeDocumentationSnippet(result, writer); + Map model = new HashMap<>(); + model.put("links", createLinksModel()); + return model; } private Map> extractLinks(MvcResult result) throws IOException { @@ -111,16 +111,6 @@ private void validate(Map> links) { } } - private void writeDocumentationSnippet(MvcResult result, PrintWriter writer) - throws IOException { - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - Map context = new HashMap<>(); - context.put("links", createLinksModel()); - context.putAll(getAttributes()); - writer.print(templateEngine.compileTemplate("links").render(context)); - } - private List> createLinksModel() { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { 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 index 3bc1701f2..0eb0b1f3b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -17,7 +17,6 @@ package org.springframework.restdocs.payload; import java.io.IOException; -import java.io.PrintWriter; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; @@ -27,7 +26,6 @@ import java.util.Map.Entry; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -50,14 +48,11 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private final ObjectMapper objectMapper = new ObjectMapper(); - private final String templateName; - private List fieldDescriptors; FieldSnippetResultHandler(String identifier, String type, Map attributes, List descriptors) { super(identifier, type + "-fields", attributes); - this.templateName = type + "-fields"; for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); @@ -67,15 +62,12 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand } @Override - protected void handle(MvcResult result, PrintWriter writer) throws IOException { - + protected Map doHandle(MvcResult result) throws IOException { this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - Object payload = extractPayload(result); - - Map context = new HashMap<>(); + Map model = new HashMap<>(); List> fields = new ArrayList<>(); - context.put("fields", fields); + model.put("fields", fields); for (Entry entry : this.descriptorsByPath.entrySet()) { FieldDescriptor descriptor = entry.getValue(); if (descriptor.getType() == null) { @@ -83,10 +75,7 @@ protected void handle(MvcResult result, PrintWriter writer) throws IOException { } fields.add(descriptor.toModel()); } - context.putAll(getAttributes()); - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - writer.print(templateEngine.compileTemplate(this.templateName).render(context)); + return model; } private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index ad45e62b7..b61e85c8b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -17,7 +17,6 @@ package org.springframework.restdocs.request; import java.io.IOException; -import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -29,7 +28,6 @@ import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -54,9 +52,16 @@ protected QueryParametersSnippetResultHandler(String identifier, } @Override - protected void handle(MvcResult result, PrintWriter writer) throws IOException { + protected Map doHandle(MvcResult result) throws IOException { verifyParameterDescriptors(result); - documentParameters(result, writer); + + Map model = new HashMap<>(); + List> parameters = new ArrayList<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + parameters.add(entry.getValue().toModel()); + } + model.put("parameters", parameters); + return model; } private void verifyParameterDescriptors(MvcResult result) { @@ -88,18 +93,4 @@ private void verifyParameterDescriptors(MvcResult result) { Assert.isTrue(actualParameters.equals(expectedParameters)); } - private void documentParameters(MvcResult result, PrintWriter writer) - throws IOException { - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); - Map context = new HashMap<>(); - List> parameters = new ArrayList<>(); - for (Entry entry : this.descriptorsByName.entrySet()) { - parameters.add(entry.getValue().toModel()); - } - context.put("parameters", parameters); - context.putAll(getAttributes()); - writer.print(templateEngine.compileTemplate("query-parameters").render(context)); - } - } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index c75c351ec..12b364eb1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -17,11 +17,11 @@ package org.springframework.restdocs.snippet; import java.io.IOException; -import java.io.PrintWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -47,20 +47,19 @@ protected SnippetWritingResultHandler(String identifier, String snippetName, } } - protected abstract void handle(MvcResult result, PrintWriter writer) - throws IOException; - @Override public void handle(MvcResult result) throws IOException { WriterResolver writerResolver = (WriterResolver) result.getRequest() .getAttribute(WriterResolver.class.getName()); try (Writer writer = writerResolver.resolve(this.identifier, this.snippetName)) { - handle(result, new PrintWriter(writer)); + Map model = doHandle(result); + model.putAll(this.attributes); + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + writer.append(templateEngine.compileTemplate(this.snippetName).render(model)); } } - protected Map getAttributes() { - return this.attributes; - } + protected abstract Map doHandle(MvcResult result) throws IOException; } \ No newline at end of file From 1175ca91e25558f6bcced9d97e1c0e61754b3e69 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jul 2015 16:16:46 +0100 Subject: [PATCH 0101/1059] Fix package tangles --- docs/src/test/java/com/example/Payload.java | 7 +++---- .../restdocs/config/RestDocumentationConfigurer.java | 1 - .../RestDocumentationContextPlaceholderResolver.java | 3 +-- .../springframework/restdocs/curl/CurlDocumentation.java | 2 +- .../springframework/restdocs/http/HttpDocumentation.java | 2 +- .../restdocs/hypermedia/LinkDescriptor.java | 2 +- .../springframework/restdocs/payload/FieldDescriptor.java | 2 +- .../restdocs/request/ParameterDescriptor.java | 2 +- .../restdocs/{ => snippet}/AbstractDescriptor.java | 4 ++-- .../springframework/restdocs/{ => snippet}/Attributes.java | 2 +- .../{util => snippet}/DocumentableHttpServletRequest.java | 2 +- .../restdocs/RestDocumentationIntegrationTests.java | 4 ++-- .../restdocs/curl/CurlDocumentationTests.java | 4 ++-- .../restdocs/http/HttpDocumentationTests.java | 4 ++-- .../restdocs/hypermedia/HypermediaDocumentationTests.java | 4 ++-- .../restdocs/payload/PayloadDocumentationTests.java | 4 ++-- .../restdocs/request/RequestDocumentationTests.java | 4 ++-- .../RestDocumentationContextPlaceholderResolverTests.java | 1 + .../org/springframework/restdocs/test/StubMvcResult.java | 2 +- 19 files changed, 27 insertions(+), 29 deletions(-) rename spring-restdocs/src/main/java/org/springframework/restdocs/{snippet => config}/RestDocumentationContextPlaceholderResolver.java (96%) rename spring-restdocs/src/main/java/org/springframework/restdocs/{ => snippet}/AbstractDescriptor.java (88%) rename spring-restdocs/src/main/java/org/springframework/restdocs/{ => snippet}/Attributes.java (98%) rename spring-restdocs/src/main/java/org/springframework/restdocs/{util => snippet}/DocumentableHttpServletRequest.java (99%) diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 04c32b365..7dc57552f 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -17,17 +17,16 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.Attributes.key; import org.springframework.http.MediaType; -import org.springframework.restdocs.Attributes; import org.springframework.restdocs.payload.FieldType; +import org.springframework.restdocs.snippet.Attributes; import org.springframework.test.web.servlet.MockMvc; public class Payload { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 0977b68d1..ba9304208 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,7 +21,6 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java similarity index 96% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java index e78b50f63..337caf862 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.restdocs.snippet; +package org.springframework.restdocs.config; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; /** diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 9682fec50..12b40b7e6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -24,8 +24,8 @@ import java.util.Map; import java.util.Map.Entry; +import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 474f82f28..b24618bb3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -28,8 +28,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 2c1451dc7..06689592d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -19,7 +19,7 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.AbstractDescriptor; +import org.springframework.restdocs.snippet.AbstractDescriptor; /** * A description of a link found in a hypermedia API diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index be485d625..0a14327e3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -19,7 +19,7 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.AbstractDescriptor; +import org.springframework.restdocs.snippet.AbstractDescriptor; /** * A description of a field found in a request or response payload diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index abc4ee0b0..3eb82ba20 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -19,7 +19,7 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.AbstractDescriptor; +import org.springframework.restdocs.snippet.AbstractDescriptor; /** * A descriptor of a parameter in a query string diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java similarity index 88% rename from spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index 70150bff6..d217b002a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -1,9 +1,9 @@ -package org.springframework.restdocs; +package org.springframework.restdocs.snippet; import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.Attributes.Attribute; +import org.springframework.restdocs.snippet.Attributes.Attribute; /** * Base class for descriptors. Provides the ability to associate arbitrary attributes with diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java similarity index 98% rename from spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java index 0ee204abc..9f552e1e5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/Attributes.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.snippet; import java.util.HashMap; import java.util.Map; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java similarity index 99% rename from spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java index 0aff84b70..7c2433999 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.util; +package org.springframework.restdocs.snippet; import static org.springframework.restdocs.util.IterableEnumeration.iterable; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 95049d786..660dc688e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -20,14 +20,14 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; import static org.springframework.restdocs.response.ResponsePostProcessors.replacePattern; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 74f70e446..acaff8b98 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -19,9 +19,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index c10612849..5e9e183c4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -21,10 +21,10 @@ import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.OK; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.StubMvcResult.result; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 87e3c3df9..6fb0d58da 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -20,9 +20,9 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index ab4e65f1e..4c017b044 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -20,11 +20,11 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index 85490138f..4aae853d4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -20,10 +20,10 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.Attributes.attributes; -import static org.springframework.restdocs.Attributes.key; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 468b016b7..03b216416 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -9,6 +9,7 @@ import org.junit.Test; import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver; import org.springframework.test.context.TestContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 441398a25..d772f7f0a 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -20,7 +20,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.restdocs.config.RestDocumentationContext; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; From e5c992c2492e8a4caba6182d3a2be1ebf01813fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jul 2015 17:44:50 +0100 Subject: [PATCH 0102/1059] Upgrade to Spring Framework 4.1.7.RELEASE --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bebb68e92..f44023b25 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ allprojects { apply plugin: 'samples' ext { - springVersion = '4.1.5.RELEASE' + springVersion = '4.1.7.RELEASE' jmustacheVersion = '1.10' } From 8e2fc7f45d0f77fa8c98f9bd6053642fb0a2f128 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Jul 2015 14:26:57 +0100 Subject: [PATCH 0103/1059] Provide support for documenting path parameters This commit adds support for documenting a request's path parameters. For example: mockMvc.perform( get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) .andExpect(status().isOk()) .andDo(document("locations").withPathParameters( parameterWithName("latitude") .description("The location's latitude"), parameterWithName("longitude") .description("The location's longitude") )); Closes gh-86 --- docs/build.gradle | 1 + .../docs/asciidoc/documenting-your-api.adoc | 30 +- .../src/test/java/com/example/Hypermedia.java | 2 +- .../test/java/com/example/InvokeService.java | 2 +- .../test/java/com/example/PathParameters.java | 41 +++ docs/src/test/java/com/example/Payload.java | 4 +- .../java/com/example/QueryParameters.java | 2 +- .../com/example/ResponsePostProcessing.java | 2 +- .../com/example/notes/ApiDocumentation.java | 8 +- .../notes/GettingStartedDocumentation.java | 10 +- .../com/example/notes/ApiDocumentation.java | 6 +- .../notes/GettingStartedDocumentation.java | 10 +- .../RestDocumentationRequestBuilders.java | 265 ++++++++++++++++++ .../RestDocumentationResultHandler.java | 40 +++ ...bstractParametersSnippetResultHandler.java | 67 +++++ .../PathParametersSnippetResultHandler.java | 90 ++++++ .../QueryParametersSnippetResultHandler.java | 71 ++--- .../request/RequestDocumentation.java | 16 ++ .../templates/default-path-parameters.snippet | 9 + .../RestDocumentationIntegrationTests.java | 2 +- ...RestDocumentationRequestBuildersTests.java | 157 +++++++++++ .../restdocs/curl/CurlDocumentationTests.java | 10 +- .../restdocs/http/HttpDocumentationTests.java | 10 +- .../payload/PayloadDocumentationTests.java | 4 +- .../request/RequestDocumentationTests.java | 139 +++++++-- .../restdocs/test/ExpectedSnippet.java | 5 + .../RestDocumentationRequestBuilders.java | 18 -- .../restdocs/test/TestRequestBuilders.java | 35 +++ .../path-parameters-with-extra-column.snippet | 10 + .../path-parameters-with-title.snippet | 10 + 30 files changed, 942 insertions(+), 134 deletions(-) create mode 100644 docs/src/test/java/com/example/PathParameters.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationRequestBuildersTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet diff --git a/docs/build.gradle b/docs/build.gradle index 68c54bcbb..92d3a8970 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -18,4 +18,5 @@ asciidoctor { } attributes 'revnumber': project.version, 'branch-or-tag': project.version.endsWith('SNAPSHOT') ? 'master': "v${project.version}" + inputs.files(sourceSets.test.java) } \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 3915fecbe..8a526df72 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -196,6 +196,32 @@ is not found in the request. +[[documenting-your-api-path-parameters]] +=== Path parameters + +A request's path parameters can be documented using `withPathParameters` + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] +---- +<1> Build the request. Uses the static `get` method on `RestDocumentationRequestBuilders`. +<2> Use `withPathParameters` to describe the path parameters. +<3> Document a parameter named `longitude`. Uses the static `parameterWithName` method on +`org.springframework.restdocs.request.RequestDocumentation`. +<4> Document a parameter named `latitude`. + +The result is a snippet named `path-parameters.adoc` that contains a table describing +the path parameters that are supported by the resource. + +When documenting path parameters, the test will fail if an undocumented path parameter +is used in the request. Similarly, the test will also fail if a documented path parameter +is not found in the request. + +TIP: To make the path parameters available for documentation, the request must be +built using one of the methods on `RestDocumentationRequestBuilders` rather than +`MockMvcRequestBuilders`. + [[documenting-your-api-default-snippets]] === Default snippets @@ -286,8 +312,8 @@ override the template for the `curl-request.adoc` snippet, create a template nam There are two ways to provide extra information for inclusion in a generated snippet: -. Use the `attributes` method on a field, link or query parameter descriptor to add one or - more attributes to an individual descriptor +. Use the `attributes` method on a field, link or parameter descriptor to add one or more + attributes to an individual descriptor. . Pass in some attributes when calling `withCurlRequest`, `withHttpRequest`, `withHttpResponse`, etc on `RestDocumentationResultHandler`. Such attributes will be associated with the snippet as a whole. diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java index a9e0de175..09b8e2d2e 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -17,7 +17,7 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.LinkExtractors.halLinks; diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/InvokeService.java index 18fc85088..32fdaf65c 100644 --- a/docs/src/test/java/com/example/InvokeService.java +++ b/docs/src/test/java/com/example/InvokeService.java @@ -17,7 +17,7 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/PathParameters.java new file mode 100644 index 000000000..4260b6d78 --- /dev/null +++ b/docs/src/test/java/com/example/PathParameters.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 com.example; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.test.web.servlet.MockMvc; + +public class PathParameters { + + private MockMvc mockMvc; + + public void pathParameters() throws Exception { + // tag::path-parameters[] + this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // <1> + .andExpect(status().isOk()) + .andDo(document("locations").withPathParameters( // <2> + parameterWithName("latitude").description("The location's latitude"), // <3> + parameterWithName("longitude").description("The location's longitude") // <4> + )); + // end::path-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 7dc57552f..121dd114c 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -20,8 +20,8 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; diff --git a/docs/src/test/java/com/example/QueryParameters.java b/docs/src/test/java/com/example/QueryParameters.java index ca81142c0..f4af60379 100644 --- a/docs/src/test/java/com/example/QueryParameters.java +++ b/docs/src/test/java/com/example/QueryParameters.java @@ -18,7 +18,7 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.MockMvc; diff --git a/docs/src/test/java/com/example/ResponsePostProcessing.java b/docs/src/test/java/com/example/ResponsePostProcessing.java index c9a6bcb01..a0c8d8ae0 100644 --- a/docs/src/test/java/com/example/ResponsePostProcessing.java +++ b/docs/src/test/java/com/example/ResponsePostProcessing.java @@ -17,7 +17,7 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.MockMvc; 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 80a762ad2..5fa7364a2 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 @@ -22,9 +22,9 @@ 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; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -279,7 +279,7 @@ public void noteUpdateExample() throws Exception { 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 diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index aca7a580c..a6afc59fd 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -21,9 +21,9 @@ import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -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; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -155,7 +155,7 @@ String createTaggedNote(String tag) throws Exception { return noteLocation; } - void getTags(String noteTagsLocation) throws Exception { + void getTags(String noteTagsLocation) throws Exception { this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("_embedded.tags", hasSize(1))); @@ -177,7 +177,7 @@ MvcResult getTaggedExistingNote(String noteLocation) throws Exception { .andReturn(); } - void getTagsForExistingNote(String noteTagsLocation) throws Exception { + void getTagsForExistingNote(String noteTagsLocation) throws Exception { this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("_embedded.tags", hasSize(1))); 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 4f3975695..1901ea85d 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 @@ -22,9 +22,9 @@ import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; 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; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 2ca0d48bf..79b780515 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -21,9 +21,9 @@ import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -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; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -155,7 +155,7 @@ String createTaggedNote(String tag) throws Exception { return noteLocation; } - void getTags(String noteTagsLocation) throws Exception { + void getTags(String noteTagsLocation) throws Exception { this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("_embedded.tags", hasSize(1))); @@ -177,7 +177,7 @@ MvcResult getTaggedExistingNote(String noteLocation) throws Exception { .andReturn(); } - void getTagsForExistingNote(String noteTagsLocation) throws Exception { + void getTagsForExistingNote(String noteTagsLocation) throws Exception { this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("_embedded.tags", hasSize(1))); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java new file mode 100644 index 000000000..e702fdafb --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java @@ -0,0 +1,265 @@ +/* + * 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; + +import java.net.URI; + +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +/** + * A drop-in replacement for {@link MockMvcRequestBuilders} that captures a request's URL + * template and makes it available for documentation. Required when + * {@link RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs .request.ParameterDescriptor...) + * documenting path parameters} and recommended for general usage. + * + * @author Andy Wilkinson + * @see MockMvcRequestBuilders + * @see RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs.request.ParameterDescriptor...) + * @see RestDocumentationResultHandler#withPathParameters(java.util.Map, + * org.springframework.restdocs.request.ParameterDescriptor...) + */ +public abstract class RestDocumentationRequestBuilders { + + private static final String ATTRIBUTE_NAME_URL_TEMPLATE = "org.springframework.restdocs.urlTemplate"; + + private RestDocumentationRequestBuilders() { + + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a GET request. The url template + * will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the GET request + */ + public static MockHttpServletRequestBuilder get(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a GET request. + * + * @param uri the URL + * @return the builder for the GET request + */ + public static MockHttpServletRequestBuilder get(URI uri) { + return MockMvcRequestBuilders.get(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a POST request. The url template + * will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the POST request + */ + public static MockHttpServletRequestBuilder post(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a POST request. + * + * @param uri the URL + * @return the builder for the POST request + */ + public static MockHttpServletRequestBuilder post(URI uri) { + return MockMvcRequestBuilders.post(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a PUT request. The url template + * will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the PUT request + */ + public static MockHttpServletRequestBuilder put(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a PUT request. + * + * @param uri the URL + * @return the builder for the PUT request + */ + public static MockHttpServletRequestBuilder put(URI uri) { + return MockMvcRequestBuilders.put(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. The url + * template will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the PATCH request + */ + public static MockHttpServletRequestBuilder patch(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. + * + * @param uri the URL + * @return the builder for the PATCH request + */ + public static MockHttpServletRequestBuilder patch(URI uri) { + return MockMvcRequestBuilders.patch(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. The url + * template will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the DELETE request + */ + public static MockHttpServletRequestBuilder delete(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. + * + * @param uri the URL + * @return the builder for the DELETE request + */ + public static MockHttpServletRequestBuilder delete(URI uri) { + return MockMvcRequestBuilders.delete(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. The url + * template will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the OPTIONS request + */ + public static MockHttpServletRequestBuilder options(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. + * + * @param uri the URL + * @return the builder for the OPTIONS request + */ + public static MockHttpServletRequestBuilder options(URI uri) { + return MockMvcRequestBuilders.options(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. The url template + * will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the HEAD request + */ + public static MockHttpServletRequestBuilder head(String urlTemplate, + Object... urlVariables) { + return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. + * + * @param uri the URL + * @return the builder for the HEAD request + */ + public static MockHttpServletRequestBuilder head(URI uri) { + return MockMvcRequestBuilders.head(uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP + * method. The url template will be captured and made available for documentation. + * + * @param httpMethod the HTTP method + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the request + */ + public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, + String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.request(httpMethod, urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP + * method. + * @param httpMethod the HTTP method (GET, POST, etc) + * @param uri the URL + * @return the builder for the request + */ + public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI uri) { + return MockMvcRequestBuilders.request(httpMethod, uri); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a multipart request. The url + * template will be captured and made available for documentation. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + * @return the builder for the file upload request + */ + public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, + Object... urlVariables) { + return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders + .fileUpload(urlTemplate, urlVariables).requestAttr( + ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + } + + /** + * Create a {@link MockHttpServletRequestBuilder} for a multipart request. + * + * @param uri the URL + * @return the builder for the file upload request + */ + public static MockMultipartHttpServletRequestBuilder fileUpload(URI uri) { + return MockMvcRequestBuilders.fileUpload(uri); + } + +} 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 1d8a04454..a8134f87b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -22,6 +22,7 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; +import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import java.util.ArrayList; @@ -309,6 +310,45 @@ public RestDocumentationResultHandler withQueryParameters( return this; } + /** + * Documents the parameters in the request's path using the given {@code descriptors}. + *

+ * If a parameter is present in the path but is not described by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * parameter is described but is not present in the request a failure will also occur + * when this handler is invoked. + * + * @param descriptors the parameter descriptors + * @return {@code this} + * @see RequestDocumentation#parameterWithName(String) + */ + public RestDocumentationResultHandler withPathParameters( + ParameterDescriptor... descriptors) { + return this.withQueryParameters(null, descriptors); + } + + /** + * Documents the parameters in the request's path using the given {@code descriptors}. + * The given {@code attributes} are made available during the generation of the path + * parameters snippet. + *

+ * If a parameter is present in the path but is not described by one of the + * descriptors a failure will occur when this handler is invoked. Similarly, if a + * parameter is described but is not present in the request a failure will also occur + * when this handler is invoked. + * + * @param descriptors the parameter descriptors + * @param attributes the attributes + * @return {@code this} + * @see RequestDocumentation#parameterWithName(String) + */ + public RestDocumentationResultHandler withPathParameters( + Map attributes, ParameterDescriptor... descriptors) { + this.delegates.add(documentPathParameters(this.identifier, attributes, + descriptors)); + return this; + } + @Override public void handle(MvcResult result) throws Exception { this.curlRequest.handle(result); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java new file mode 100644 index 000000000..cc4e630bc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java @@ -0,0 +1,67 @@ +package org.springframework.restdocs.request; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.Assert; + +public abstract class AbstractParametersSnippetResultHandler extends + SnippetWritingResultHandler { + + private final Map descriptorsByName = new LinkedHashMap<>(); + + protected AbstractParametersSnippetResultHandler(String identifier, + String snippetName, Map attributes, + ParameterDescriptor... descriptors) { + super(identifier, snippetName, attributes); + for (ParameterDescriptor descriptor : descriptors) { + Assert.hasText(descriptor.getName()); + Assert.hasText(descriptor.getDescription()); + this.descriptorsByName.put(descriptor.getName(), descriptor); + } + } + + @Override + protected Map doHandle(MvcResult result) throws IOException { + verifyParameterDescriptors(result); + + Map model = new HashMap<>(); + List> parameters = new ArrayList<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + parameters.add(entry.getValue().toModel()); + } + model.put("parameters", parameters); + return model; + } + + protected void verifyParameterDescriptors(MvcResult result) { + Set actualParameters = extractActualParameters(result); + Set expectedParameters = this.descriptorsByName.keySet(); + Set undocumentedParameters = new HashSet(actualParameters); + undocumentedParameters.removeAll(expectedParameters); + Set missingParameters = new HashSet(expectedParameters); + missingParameters.removeAll(actualParameters); + + if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { + verificationFailed(undocumentedParameters, missingParameters); + } + else { + Assert.isTrue(actualParameters.equals(expectedParameters)); + } + } + + protected abstract Set extractActualParameters(MvcResult result); + + protected abstract void verificationFailed(Set undocumentedParameters, + Set missingParameters); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java new file mode 100644 index 000000000..70298a353 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java @@ -0,0 +1,90 @@ +/* + * 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.request; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.restdocs.snippet.SnippetGenerationException; +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 the path + * parameters supported by a RESTful resource. + * + * @author Andy Wilkinson + */ +public class PathParametersSnippetResultHandler extends + AbstractParametersSnippetResultHandler { + + private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + + protected PathParametersSnippetResultHandler(String identifier, + Map attributes, ParameterDescriptor... descriptors) { + super(identifier, "path-parameters", attributes, descriptors); + } + + @Override + protected Set extractActualParameters(MvcResult result) { + String urlTemplate = extractUrlTemplate(result); + Matcher matcher = NAMES_PATTERN.matcher(urlTemplate); + Set actualParameters = new HashSet<>(); + while (matcher.find()) { + String match = matcher.group(1); + actualParameters.add(getParameterName(match)); + } + return actualParameters; + } + + private String extractUrlTemplate(MvcResult result) { + String urlTemplate = (String) result.getRequest().getAttribute( + "org.springframework.restdocs.urlTemplate"); + Assert.notNull(urlTemplate, + "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " + + "build the request?"); + return urlTemplate; + } + + private static String getParameterName(String match) { + int colonIdx = match.indexOf(':'); + return (colonIdx != -1 ? match.substring(0, colonIdx) : match); + } + + @Override + protected void verificationFailed(Set undocumentedParameters, + Set missingParameters) { + String message = ""; + if (!undocumentedParameters.isEmpty()) { + message += "Path parameters with the following names were not documented: " + + undocumentedParameters; + } + if (!missingParameters.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Path parameters with the following names were not found in " + + "the request: " + missingParameters; + } + throw new SnippetGenerationException(message); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index b61e85c8b..45a3f9246 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -16,20 +16,12 @@ package org.springframework.restdocs.request; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.SnippetGenerationException; 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 the query @@ -37,60 +29,35 @@ * * @author Andy Wilkinson */ -public class QueryParametersSnippetResultHandler extends SnippetWritingResultHandler { - - private final Map descriptorsByName = new LinkedHashMap<>(); +public class QueryParametersSnippetResultHandler extends + AbstractParametersSnippetResultHandler { protected QueryParametersSnippetResultHandler(String identifier, Map attributes, ParameterDescriptor... descriptors) { - super(identifier, "query-parameters", attributes); - for (ParameterDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getName()); - Assert.hasText(descriptor.getDescription()); - this.descriptorsByName.put(descriptor.getName(), descriptor); - } + super(identifier, "query-parameters", attributes, descriptors); } @Override - protected Map doHandle(MvcResult result) throws IOException { - verifyParameterDescriptors(result); - - Map model = new HashMap<>(); - List> parameters = new ArrayList<>(); - for (Entry entry : this.descriptorsByName.entrySet()) { - parameters.add(entry.getValue().toModel()); + protected void verificationFailed(Set undocumentedParameters, + Set missingParameters) { + String message = ""; + if (!undocumentedParameters.isEmpty()) { + message += "Query parameters with the following names were not documented: " + + undocumentedParameters; } - model.put("parameters", parameters); - return model; - } - - private void verifyParameterDescriptors(MvcResult result) { - Set actualParameters = result.getRequest().getParameterMap().keySet(); - Set expectedParameters = this.descriptorsByName.keySet(); - - Set undocumentedParameters = new HashSet(actualParameters); - undocumentedParameters.removeAll(expectedParameters); - - Set missingParameters = new HashSet(expectedParameters); - missingParameters.removeAll(actualParameters); - - if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { - String message = ""; - if (!undocumentedParameters.isEmpty()) { - message += "Query parameters with the following names were not documented: " - + undocumentedParameters; - } - if (!missingParameters.isEmpty()) { - if (message.length() > 0) { - message += ". "; - } - message += "Query parameters with the following names were not found in the request: " - + missingParameters; + if (!missingParameters.isEmpty()) { + if (message.length() > 0) { + message += ". "; } - throw new SnippetGenerationException(message); + message += "Query parameters with the following names were not found in the request: " + + missingParameters; } + throw new SnippetGenerationException(message); + } - Assert.isTrue(actualParameters.equals(expectedParameters)); + @Override + protected Set extractActualParameters(MvcResult result) { + return result.getRequest().getParameterMap().keySet(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index aadcf76ac..3b45c3ecd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -32,6 +32,22 @@ private RequestDocumentation() { } + /** + * Creates a {@link SnippetWritingResultHandler} that will produce a snippet + * documenting a request's path parameters. + * + * @param identifier An identifier for the API call that is being documented + * @param attributes Attributes made available during rendering of the path parameters + * snippet + * @param descriptors The descriptions of the parameters in the request's path + * @return the result handler + * @see RestDocumentationResultHandler#withPathParameters(ParameterDescriptor...) + */ + public static SnippetWritingResultHandler documentPathParameters(String identifier, + Map attributes, ParameterDescriptor... descriptors) { + return new PathParametersSnippetResultHandler(identifier, attributes, descriptors); + } + /** * Creates a {@link SnippetWritingResultHandler} that will produce a snippet * documenting a request's query parameters diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet new file mode 100644 index 000000000..067e1fb83 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 660dc688e..b2ba9bb63 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; @@ -30,7 +31,6 @@ import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.File; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationRequestBuildersTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationRequestBuildersTests.java new file mode 100644 index 000000000..aebc1feba --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationRequestBuildersTests.java @@ -0,0 +1,157 @@ +/* + * 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; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.head; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.options; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.request; + +import java.net.URI; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +/** + * Tests for {@link RestDocumentationRequestBuilders} + * + * @author Andy Wilkinson + * + */ +public class RestDocumentationRequestBuildersTests { + + private final ServletContext servletContext = new MockServletContext(); + + @Test + public void getTemplate() { + assertTemplate(get("{template}", "t"), HttpMethod.GET); + } + + @Test + public void getUri() { + assertUri(get(URI.create("/uri")), HttpMethod.GET); + } + + @Test + public void postTemplate() { + assertTemplate(post("{template}", "t"), HttpMethod.POST); + } + + @Test + public void postUri() { + assertUri(post(URI.create("/uri")), HttpMethod.POST); + } + + @Test + public void putTemplate() { + assertTemplate(put("{template}", "t"), HttpMethod.PUT); + } + + @Test + public void putUri() { + assertUri(put(URI.create("/uri")), HttpMethod.PUT); + } + + @Test + public void patchTemplate() { + assertTemplate(patch("{template}", "t"), HttpMethod.PATCH); + } + + @Test + public void patchUri() { + assertUri(patch(URI.create("/uri")), HttpMethod.PATCH); + } + + @Test + public void deleteTemplate() { + assertTemplate(delete("{template}", "t"), HttpMethod.DELETE); + } + + @Test + public void deleteUri() { + assertUri(delete(URI.create("/uri")), HttpMethod.DELETE); + } + + @Test + public void optionsTemplate() { + assertTemplate(options("{template}", "t"), HttpMethod.OPTIONS); + } + + @Test + public void optionsUri() { + assertUri(options(URI.create("/uri")), HttpMethod.OPTIONS); + } + + @Test + public void headTemplate() { + assertTemplate(head("{template}", "t"), HttpMethod.HEAD); + } + + @Test + public void headUri() { + assertUri(head(URI.create("/uri")), HttpMethod.HEAD); + } + + @Test + public void requestTemplate() { + assertTemplate(request(HttpMethod.GET, "{template}", "t"), HttpMethod.GET); + } + + @Test + public void requestUri() { + assertUri(request(HttpMethod.GET, URI.create("/uri")), HttpMethod.GET); + } + + @Test + public void fileUploadTemplate() { + assertTemplate(fileUpload("{template}", "t"), HttpMethod.POST); + } + + @Test + public void fileUploadUri() { + assertUri(fileUpload(URI.create("/uri")), HttpMethod.POST); + } + + private void assertTemplate(MockHttpServletRequestBuilder builder, + HttpMethod httpMethod) { + MockHttpServletRequest request = builder.buildRequest(this.servletContext); + assertThat( + (String) request.getAttribute("org.springframework.restdocs.urlTemplate"), + is(equalTo("{template}"))); + assertThat(request.getRequestURI(), is(equalTo("t"))); + assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); + } + + private void assertUri(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { + MockHttpServletRequest request = builder.buildRequest(this.servletContext); + assertThat(request.getRequestURI(), is(equalTo("/uri"))); + assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index acaff8b98..c0b8f808d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -19,15 +19,15 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import java.io.IOException; @@ -45,7 +45,7 @@ /** * Tests for {@link CurlDocumentation} - * + * * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java index 5e9e183c4..59b1f5476 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -21,6 +21,10 @@ import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.OK; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -28,10 +32,6 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; import static org.springframework.web.bind.annotation.RequestMethod.PUT; @@ -53,7 +53,7 @@ /** * Tests for {@link HttpDocumentation} - * + * * @author Andy Wilkinson * @author Jonathan Pearlin */ diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 4c017b044..f23a3ba65 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -20,6 +20,7 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -27,7 +28,6 @@ import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import java.io.IOException; @@ -45,7 +45,7 @@ /** * Tests for {@link PayloadDocumentation} - * + * * @author Andy Wilkinson */ public class PayloadDocumentationTests { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index 4aae853d4..b193ad5c0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -20,13 +20,14 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.restdocs.test.TestRequestBuilders.get; import java.io.IOException; @@ -56,56 +57,56 @@ public class RequestDocumentationTests { public ExpectedSnippet snippet = new ExpectedSnippet(); @Test - public void undocumentedParameter() throws IOException { + public void undocumentedQueryParameter() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(equalTo("Query parameters with the following names were" + " not documented: [a]")); - documentQueryParameters("undocumented-parameter", null).handle( + documentQueryParameters("undocumented-query-parameter", null).handle( result(get("/").param("a", "alpha"))); } @Test - public void missingParameter() throws IOException { + public void missingQueryParameter() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(equalTo("Query parameters with the following names were" + " not found in the request: [a]")); - documentQueryParameters("missing-parameter", null, + documentQueryParameters("missing-query-parameter", null, parameterWithName("a").description("one")).handle(result(get("/"))); } @Test - public void undocumentedParameterAndMissingParameter() throws IOException { + public void undocumentedAndMissingQueryParameters() throws IOException { this.thrown.expect(SnippetGenerationException.class); this.thrown .expectMessage(equalTo("Query parameters with the following names were" + " not documented: [b]. Query parameters with the following" + " names were not found in the request: [a]")); - documentQueryParameters("undocumented-parameter-missing-parameter", null, + documentQueryParameters("undocumented-and-missing-query-parameters", null, parameterWithName("a").description("one")).handle( result(get("/").param("b", "bravo"))); } @Test - public void parameterSnippetFromRequestParameters() throws IOException { - this.snippet.expectQueryParameters("parameter-snippet-request-parameters") + public void queryParameterSnippetFromRequestParameters() throws IOException { + this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters") .withContents( tableWithHeader("Parameter", "Description").row("a", "one").row( "b", "two")); - documentQueryParameters("parameter-snippet-request-parameters", null, + documentQueryParameters("query-parameter-snippet-request-parameters", null, parameterWithName("a").description("one"), parameterWithName("b").description("two")).handle( result(get("/").param("a", "bravo").param("b", "bravo"))); } @Test - public void parameterSnippetFromRequestUriQueryString() throws IOException { - this.snippet.expectQueryParameters("parameter-snippet-request-uri-query-string") - .withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row( - "b", "two")); - documentQueryParameters("parameter-snippet-request-uri-query-string", null, + public void queryParameterSnippetFromRequestUriQueryString() throws IOException { + this.snippet.expectQueryParameters( + "query-parameter-snippet-request-uri-query-string").withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row("b", + "two")); + documentQueryParameters("query-parameter-snippet-request-uri-query-string", null, parameterWithName("a").description("one"), parameterWithName("b").description("two")).handle( result(get("/?a=alpha&b=bravo").requestAttr( @@ -114,12 +115,11 @@ public void parameterSnippetFromRequestUriQueryString() throws IOException { } @Test - public void parametersWithCustomDescriptorAttributes() throws IOException { - this.snippet - .expectQueryParameters("parameters-with-custom-descriptor-attributes") - .withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", - "one", "alpha").row("b", "two", "bravo")); + public void queryParametersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectQueryParameters( + "query-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("query-parameters")) .thenReturn( @@ -131,7 +131,7 @@ public void parametersWithCustomDescriptorAttributes() throws IOException { request.addParameter("a", "bravo"); request.addParameter("b", "bravo"); documentQueryParameters( - "parameters-with-custom-descriptor-attributes", + "query-parameters-with-custom-descriptor-attributes", null, parameterWithName("a").description("one").attributes( key("foo").value("alpha")), @@ -140,8 +140,8 @@ public void parametersWithCustomDescriptorAttributes() throws IOException { } @Test - public void parametersWithCustomAttributes() throws IOException { - this.snippet.expectQueryParameters("parameters-with-custom-attributes") + public void queryParametersWithCustomAttributes() throws IOException { + this.snippet.expectQueryParameters("query-parameters-with-custom-attributes") .withContents(startsWith(".The title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("query-parameters")) @@ -154,7 +154,7 @@ public void parametersWithCustomAttributes() throws IOException { request.addParameter("a", "bravo"); request.addParameter("b", "bravo"); documentQueryParameters( - "parameters-with-custom-attributes", + "query-parameters-with-custom-attributes", attributes(key("title").value("The title")), parameterWithName("a").description("one").attributes( key("foo").value("alpha")), @@ -162,4 +162,91 @@ public void parametersWithCustomAttributes() throws IOException { key("foo").value("bravo"))).handle(result(request)); } + + @Test + public void undocumentedPathParameter() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [a]")); + documentPathParameters("undocumented-path-parameter", null).handle( + result(get("/{a}/", "alpha"))); + } + + @Test + public void missingPathParameter() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not found in the request: [a]")); + documentPathParameters("missing-path-parameter", null, + parameterWithName("a").description("one")).handle(result(get("/"))); + } + + @Test + public void undocumentedAndMissingPathParameters() throws IOException { + this.thrown.expect(SnippetGenerationException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [b]. Path parameters with the following" + + " names were not found in the request: [a]")); + documentPathParameters("undocumented-and-missing-path-parameters", null, + parameterWithName("a").description("one")).handle( + result(get("/{b}", "bravo"))); + } + + @Test + public void pathParameters() throws IOException { + this.snippet.expectPathParameters("path-parameters").withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row("b", + "two")); + documentPathParameters("path-parameters", null, + parameterWithName("a").description("one"), + parameterWithName("b").description("two")).handle( + result(get("/{a}/{b}", "alpha", "banana").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext()))); + } + + @Test + public void pathParametersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectPathParameters( + "path-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("path-parameters")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet")); + documentPathParameters( + "path-parameters-with-custom-descriptor-attributes", + null, + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo"))).handle( + result(get("{a}/{b}", "alpha", "bravo").requestAttr( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)))); + } + + @Test + public void pathParametersWithCustomAttributes() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("path-parameters")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet")); + documentPathParameters( + "path-parameters-with-custom-attributes", + attributes(key("title").value("The title")), + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo"))).handle( + result(get("{a}/{b}", "alpha", "bravo").requestAttr( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)))); + + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 1bc2dda0b..5847424c5 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -135,6 +135,11 @@ public ExpectedSnippet expectQueryParameters(String name) { return this; } + public ExpectedSnippet expectPathParameters(String name) { + expect(name, "path-parameters"); + return this; + } + private ExpectedSnippet expect(String name, String type) { this.expectedName = name; this.expectedType = type; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java deleted file mode 100644 index 05bcd7b47..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/RestDocumentationRequestBuilders.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.springframework.restdocs.test; - -import org.springframework.restdocs.config.RestDocumentationContext; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -public class RestDocumentationRequestBuilders { - - private RestDocumentationRequestBuilders() { - - } - - public static MockHttpServletRequestBuilder get(String urlTemplate) { - return MockMvcRequestBuilders.get(urlTemplate).requestAttr( - RestDocumentationContext.class.getName(), new RestDocumentationContext()); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java new file mode 100644 index 000000000..de6d5df0c --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java @@ -0,0 +1,35 @@ +/* + * 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.test; + +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +public class TestRequestBuilders { + + private TestRequestBuilders() { + + } + + public static MockHttpServletRequestBuilder get(String urlTemplate, + Object... urlVariables) { + return org.springframework.restdocs.RestDocumentationRequestBuilders.get( + urlTemplate, urlVariables).requestAttr( + RestDocumentationContext.class.getName(), new RestDocumentationContext()); + } + +} diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet new file mode 100644 index 000000000..fb97f89bf --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Description|Foo + +{{#parameters}} +|{{name}} +|{{description}} +|{{foo}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet new file mode 100644 index 000000000..611254aa9 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file From dc30f1bd633c37792f50e452e7c604d29ee0a482 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Wed, 29 Jul 2015 17:14:00 +0200 Subject: [PATCH 0104/1059] Fix RestDocumentationResultHandler.withPathParameters Closes gh-101 --- .../restdocs/RestDocumentationResultHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a8134f87b..c3930548c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -324,7 +324,7 @@ public RestDocumentationResultHandler withQueryParameters( */ public RestDocumentationResultHandler withPathParameters( ParameterDescriptor... descriptors) { - return this.withQueryParameters(null, descriptors); + return this.withPathParameters(null, descriptors); } /** From 458718c44389bf0ccd8f9d488633b8839361a1d3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Jul 2015 16:25:04 +0100 Subject: [PATCH 0105/1059] Improve test coverage of RestDocumentationResultHandler --- .../restdocs/templates/default-fields.snippet | 10 --- .../RestDocumentationIntegrationTests.java | 81 +++++++++++++++++++ 2 files changed, 81 insertions(+), 10 deletions(-) delete mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet deleted file mode 100644 index 497f5a03a..000000000 --- a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet +++ /dev/null @@ -1,10 +0,0 @@ -|=== -|Path|Type|Description - -{{#fields}} -|{{path}} -|{{type}} -|{{description}} - -{{/fields}} -|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index b2ba9bb63..bc84de534 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -23,6 +23,9 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; @@ -108,6 +111,84 @@ public void basicSnippetGeneration() throws Exception { "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } + @Test + public void links() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links").withLinks( + linkWithRel("rel").description("The description"))); + + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "links.adoc"); + } + + @Test + public void pathParameters() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links").withPathParameters( + parameterWithName("foo").description("The description"))); + + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "path-parameters.adoc"); + } + + @Test + public void queryParameters() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links").withQueryParameters( + parameterWithName("foo").description("The description"))); + + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "query-parameters.adoc"); + } + + @Test + public void requestFields() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform( + get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links").withRequestFields( + fieldWithPath("a").description("The description"))); + + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "request-fields.adoc"); + } + + @Test + public void responseFields() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links").withResponseFields( + fieldWithPath("a").description("The description"), + fieldWithPath("links").description("Links to other resources"))); + + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "response-fields.adoc"); + } + @Test public void parameterizedOutputDirectory() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From aaf8aab77ea72d40cbb4c68de147f5f26683e248 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 29 Jul 2015 16:31:29 +0100 Subject: [PATCH 0106/1059] Polishing --- .../restdocs/request/PathParametersSnippetResultHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java index 70298a353..70465a1ab 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java @@ -65,8 +65,8 @@ private String extractUrlTemplate(MvcResult result) { } private static String getParameterName(String match) { - int colonIdx = match.indexOf(':'); - return (colonIdx != -1 ? match.substring(0, colonIdx) : match); + int colonIndex = match.indexOf(':'); + return colonIndex != -1 ? match.substring(0, colonIndex) : match; } @Override From 0c8a71370b45355689d4ab7fda4c5bc1447822f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jul 2015 16:29:25 +0100 Subject: [PATCH 0107/1059] Rework the API to improve readability and extensibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates the API to improve its extensibility and readability. SnippetWritingResultHandler has been replaced with a more general purpose Snippet interface. Snippets are now provided to the main document method using varargs rather than the various with… methods that were previously used. As a result a custom Snippet implementation can now be used in exactly the same way as any of the built-in snippets: this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andDo(document("index-example", links( linkWithRel("notes").description("…"), linkWithRel("tags").description("…")), responseFields( fieldWithPath("_links").description("…")), yourCustomSnippet())); Control of the snippets that are generated by default is now available via RestDocumentationConfigurer: this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration().snippets() .withDefaults(curlRequest(), yourCustomSnippet())) .build(); See gh-73 --- docs/src/docs/asciidoc/configuration.adoc | 20 ++ .../docs/asciidoc/documenting-your-api.adoc | 20 +- docs/src/docs/asciidoc/getting-started.adoc | 4 +- .../CustomDefaultSnippetsConfiguration.java | 29 ++ .../src/test/java/com/example/Hypermedia.java | 13 +- .../test/java/com/example/PathParameters.java | 7 +- docs/src/test/java/com/example/Payload.java | 15 +- .../java/com/example/QueryParameters.java | 7 +- .../com/example/notes/ApiDocumentation.java | 76 ++-- .../com/example/notes/ApiDocumentation.java | 75 ++-- .../restdocs/ResponseModifier.java | 16 +- .../restdocs/RestDocumentation.java | 13 +- .../RestDocumentationRequestBuilders.java | 9 +- .../RestDocumentationResultHandler.java | 334 +----------------- .../restdocs/config/AbstractConfigurer.java | 2 +- .../config/RestDocumentationConfigurer.java | 30 +- .../config/RestDocumentationContext.java | 10 +- ...cumentationContextPlaceholderResolver.java | 6 + .../restdocs/config/SnippetConfigurer.java | 25 +- .../restdocs/curl/CurlDocumentation.java | 166 +-------- .../restdocs/curl/CurlRequestSnippet.java | 171 +++++++++ .../restdocs/http/HttpDocumentation.java | 232 ++---------- .../restdocs/http/HttpRequestSnippet.java | 175 +++++++++ .../restdocs/http/HttpResponseSnippet.java | 76 ++++ .../hypermedia/AbstractJsonLinkExtractor.java | 46 +++ .../hypermedia/AtomLinkExtractor.java | 65 ++++ .../hypermedia/ContentTypeLinkExtractor.java | 58 +++ .../restdocs/hypermedia/HalLinkExtractor.java | 80 +++++ .../hypermedia/HypermediaDocumentation.java | 84 ++++- .../restdocs/hypermedia/LinkExtractors.java | 187 ---------- ...etResultHandler.java => LinksSnippet.java} | 52 +-- ...andler.java => AbstractFieldsSnippet.java} | 21 +- .../restdocs/payload/FieldValidator.java | 4 +- .../payload/PayloadDocumentation.java | 93 +++-- ...Handler.java => RequestFieldsSnippet.java} | 14 +- ...andler.java => ResponseFieldsSnippet.java} | 13 +- ...er.java => AbstractParametersSnippet.java} | 15 +- ...andler.java => PathParametersSnippet.java} | 23 +- ...ndler.java => QueryParametersSnippet.java} | 23 +- .../request/RequestDocumentation.java | 78 ++-- .../ContentModifyingReponsePostProcessor.java | 2 +- .../HeaderRemovingResponsePostProcessor.java | 2 +- .../restdocs/snippet/Attributes.java | 17 +- .../restdocs/snippet/Snippet.java | 40 +++ ...onException.java => SnippetException.java} | 7 +- ...sultHandler.java => TemplatedSnippet.java} | 21 +- .../RestDocumentationIntegrationTests.java | 47 ++- .../RestDocumentationConfigurerTests.java | 2 +- .../config/RestDocumentationContexts.java | 6 + ...CurlRequestDocumentationHandlerTests.java} | 81 +++-- ...HttpRequestDocumentationHandlerTests.java} | 106 ++---- ...HttpResponseDocumentationHandlerTests.java | 111 ++++++ .../ContentTypeLinkExtractorTests.java | 70 ++++ .../LinkExtractorsPayloadTests.java | 7 +- .../hypermedia/LinkExtractorsTests.java | 65 ---- ...va => LinksDocumentationHandlerTests.java} | 80 ++--- .../restdocs/payload/FieldValidatorTests.java | 6 +- .../payload/PayloadDocumentationTests.java | 282 --------------- ...equestFieldsDocumentationHandlerTests.java | 185 ++++++++++ ...sponseFieldsDocumentationHandlerTests.java | 156 ++++++++ ...thParametersDocumentationHandlerTests.java | 147 ++++++++ ...ryParametersDocumentationHandlerTests.java | 169 +++++++++ .../request/RequestDocumentationTests.java | 252 ------------- .../snippet/StandardWriterResolverTests.java | 1 + .../restdocs/test/ExpectedSnippet.java | 4 +- .../restdocs/test/StubMvcResult.java | 2 +- .../restdocs/test/TestRequestBuilders.java | 3 +- 67 files changed, 2318 insertions(+), 1940 deletions(-) create mode 100644 docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java rename spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/{LinkSnippetResultHandler.java => LinksSnippet.java} (65%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{FieldSnippetResultHandler.java => AbstractFieldsSnippet.java} (81%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{RequestFieldSnippetResultHandler.java => RequestFieldsSnippet.java} (71%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{ResponseFieldSnippetResultHandler.java => ResponseFieldsSnippet.java} (74%) rename spring-restdocs/src/main/java/org/springframework/restdocs/request/{AbstractParametersSnippetResultHandler.java => AbstractParametersSnippet.java} (81%) rename spring-restdocs/src/main/java/org/springframework/restdocs/request/{PathParametersSnippetResultHandler.java => PathParametersSnippet.java} (79%) rename spring-restdocs/src/main/java/org/springframework/restdocs/request/{QueryParametersSnippetResultHandler.java => QueryParametersSnippet.java} (69%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java rename spring-restdocs/src/main/java/org/springframework/restdocs/snippet/{SnippetGenerationException.java => SnippetException.java} (81%) rename spring-restdocs/src/main/java/org/springframework/restdocs/snippet/{SnippetWritingResultHandler.java => TemplatedSnippet.java} (70%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java rename spring-restdocs/src/test/java/org/springframework/restdocs/curl/{CurlDocumentationTests.java => CurlRequestDocumentationHandlerTests.java} (84%) rename spring-restdocs/src/test/java/org/springframework/restdocs/http/{HttpDocumentationTests.java => HttpRequestDocumentationHandlerTests.java} (69%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/{HypermediaDocumentationTests.java => LinksDocumentationHandlerTests.java} (69%) delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ed9b2103b..9654df1ad 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -48,6 +48,26 @@ include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] +[[configuration-default-snippets]] +=== Default snippets + +Three snippets are produced by default: + +- `curl-request` +- `http-request` +- `http-response` + +This default configuration is applied by `RestDocumentationConfigurer`. You can use its +API to change the configuration. For example, to only produce the `curl-request` snippet +by default: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/CustomDefaultSnippetsConfiguration.java[tags=custom-default-snippets] +---- + + + [[configuration-output-directory]] === Snippet output directory diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 8a526df72..0018d7d0b 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -15,7 +15,8 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: ---- include::{examples-dir}/com/example/Hypermedia.java[tag=links] ---- -<1> Use `withLinks` is to describe the expected links. +<1> Produce a snippet describing the response's links. Uses the static `links` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <3> Expect a link whose rel is `bravo`. @@ -40,14 +41,14 @@ Two link formats are understood by default: content type of the response is compatible with `application/hal+json`. If you are using Atom or HAL-format links but with a different content type you can -provide one of the built-in `LinkExtractor` implementations to `withLinks`. For example: +provide one of the built-in `LinkExtractor` implementations to `links`. For example: [source,java,indent=0] ---- include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] ---- <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on -`org.springframework.restdocs.hypermedia.LinkExtractors`. +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. If your API represents its links in a format other than Atom or HAL you can provide your own implementation of the `LinkExtractor` interface to extract the links from the @@ -176,13 +177,14 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type] [[documenting-your-api-query-parameters]] === Query parameters -A request's query parameters can be documented using `withQueryParameters` +A request's query parameters can be documented using `queryParameters` [source,java,indent=0] ---- include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters] ---- -<1> Use `withQueryParameters` to describe the query parameters. +<1> Produce a snippet describing the request's query parameters. Uses the static +`queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <2> Document a parameter named `page`. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document a parameter named `per_page`. @@ -199,14 +201,15 @@ is not found in the request. [[documenting-your-api-path-parameters]] === Path parameters -A request's path parameters can be documented using `withPathParameters` +A request's path parameters can be documented using `pathParameters` [source,java,indent=0] ---- include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] ---- <1> Build the request. Uses the static `get` method on `RestDocumentationRequestBuilders`. -<2> Use `withPathParameters` to describe the path parameters. +<2> Produce a snippet describing the request's path parameters. Uses the static +`pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document a parameter named `longitude`. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document a parameter named `latitude`. @@ -244,6 +247,9 @@ documented | Contains the HTTP response that was returned |=== +You can configure which snippets are produced by default. Please refer to the +<> for more information. + [[documentating-your-api-parameterized-output-directories]] diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index bbb0af76a..f07f9da60 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -200,7 +200,7 @@ be included in the project's jar: copy-resources - + ${project.build.outputDirectory}/static/docs @@ -245,7 +245,7 @@ The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An i of this class can be obtained from the static `documentationConfiguration()` method on `org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies sensible defaults and also provides an API for customizing the configuration. Refer to the -<> for more information. +<> for more information. diff --git a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java b/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java new file mode 100644 index 000000000..0a74af66a --- /dev/null +++ b/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java @@ -0,0 +1,29 @@ +package com.example; + +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class CustomDefaultSnippetsConfiguration { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + // tag::custom-default-snippets[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration().snippets() + .withDefaults(curlRequest())) + .build(); + // end::custom-default-snippets[] + } + +} diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java index 09b8e2d2e..103529ea4 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -20,7 +20,8 @@ import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.LinkExtractors.halLinks; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -29,13 +30,13 @@ public class Hypermedia { private MockMvc mockMvc; - public void links() throws Exception { + public void defaultExtractor() throws Exception { // tag::links[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("index").withLinks( // <1> + .andDo(document("index", links( // <1> linkWithRel("alpha").description("Link to the alpha resource"), // <2> - linkWithRel("bravo").description("Link to the bravo resource"))); // <3> + linkWithRel("bravo").description("Link to the bravo resource")))); // <3> // end::links[] } @@ -43,9 +44,9 @@ public void explicitExtractor() throws Exception { this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) //tag::explicit-extractor[] - .andDo(document("index").withLinks(halLinks(), // <1> + .andDo(document("index", links(halLinks(), // <1> linkWithRel("alpha").description("Link to the alpha resource"), - linkWithRel("bravo").description("Link to the bravo resource"))); + linkWithRel("bravo").description("Link to the bravo resource")))); // end::explicit-extractor[] } diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/PathParameters.java index 4260b6d78..8bff0a44a 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/PathParameters.java @@ -18,6 +18,7 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -27,14 +28,14 @@ public class PathParameters { private MockMvc mockMvc; - public void pathParameters() throws Exception { + public void pathParametersSnippet() throws Exception { // tag::path-parameters[] this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // <1> .andExpect(status().isOk()) - .andDo(document("locations").withPathParameters( // <2> + .andDo(document("locations", pathParameters( // <2> parameterWithName("latitude").description("The location's latitude"), // <3> parameterWithName("longitude").description("The location's longitude") // <4> - )); + ))); // end::path-parameters[] } diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 121dd114c..a317ec1cd 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -18,6 +18,8 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; @@ -26,7 +28,6 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.payload.FieldType; -import org.springframework.restdocs.snippet.Attributes; import org.springframework.test.web.servlet.MockMvc; public class Payload { @@ -37,9 +38,9 @@ public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("index").withResponseFields( // <1> + .andDo(document("index", responseFields( // <1> fieldWithPath("contact").description("The user's contact details"), // <2> - fieldWithPath("contact.email").description("The user's email address"))); // <3> + fieldWithPath("contact.email").description("The user's email address")))); // <3> // end::response[] } @@ -47,11 +48,11 @@ public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // tag::explicit-type[] - .andDo(document("index").withResponseFields( + .andDo(document("index", responseFields( fieldWithPath("contact.email") .type(FieldType.STRING) // <1> .optional() - .description("The user's email address"))); + .description("The user's email address")))); // end::explicit-type[] } @@ -59,7 +60,7 @@ public void constraints() throws Exception { this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // tag::constraints[] - .andDo(document("create-user").withRequestFields( + .andDo(document("create-user", requestFields( attributes( key("title").value("Fields for user creation")), // <1> fieldWithPath("name") @@ -69,7 +70,7 @@ public void constraints() throws Exception { fieldWithPath("email") .description("The user's email address") .attributes( - key("constraints").value("Must be a valid email address")))); // <3> + key("constraints").value("Must be a valid email address"))))); // <3> // end::constraints[] } diff --git a/docs/src/test/java/com/example/QueryParameters.java b/docs/src/test/java/com/example/QueryParameters.java index f4af60379..4afcc35dc 100644 --- a/docs/src/test/java/com/example/QueryParameters.java +++ b/docs/src/test/java/com/example/QueryParameters.java @@ -18,6 +18,7 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -27,14 +28,14 @@ public class QueryParameters { private MockMvc mockMvc; - public void queryParameters() throws Exception { + public void queryParametersSnippet() throws Exception { // tag::query-parameters[] this.mockMvc.perform(get("/users?page=2&per_page=100")) .andExpect(status().isOk()) - .andDo(document("users").withQueryParameters( // <1> + .andDo(document("users", queryParameters( // <1> parameterWithName("page").description("The page to retrieve"), // <2> parameterWithName("per_page").description("Entries per page") // <3> - )); + ))); // end::query-parameters[] } 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 5fa7364a2..f58d92adf 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 @@ -21,7 +21,10 @@ import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; @@ -100,26 +103,26 @@ public void errorExample() throws Exception { .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example") - .withResponseFields( + .andDo(document("error-example", + responseFields( 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"))); + 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( + .andDo(document("index-example", + links( linkWithRel("notes").description("The <>"), linkWithRel("tags").description("The <>"), - linkWithRel("profile").description("The ALPS profile for the service")) - .withResponseFields( - fieldWithPath("_links").description("<> to other resources"))); + linkWithRel("profile").description("The ALPS profile for the service")), + responseFields( + fieldWithPath("_links").description("<> to other resources")))); } @@ -135,9 +138,9 @@ public void notesListExample() throws Exception { this.mockMvc.perform(get("/notes")) .andExpect(status().isOk()) - .andDo(document("notes-list-example") - .withResponseFields( - fieldWithPath("_embedded.notes").description("An array of <>"))); + .andDo(document("notes-list-example", + responseFields( + fieldWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -161,11 +164,11 @@ public void notesCreateExample() throws Exception { post("/notes").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(note))).andExpect( status().isCreated()) - .andDo(document("notes-create-example") - .withRequestFields( + .andDo(document("notes-create-example", + requestFields( 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"))); + fieldWithPath("tags").description("An array of tag resource URIs")))); } @Test @@ -198,14 +201,14 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("note-get-example") - .withLinks( + .andDo(document("note-get-example", + links( linkWithRel("self").description("This <>"), - linkWithRel("tags").description("This note's tags")) - .withResponseFields( + linkWithRel("tags").description("This note's tags")), + responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources"))); + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -219,9 +222,9 @@ public void tagsListExample() throws Exception { this.mockMvc.perform(get("/tags")) .andExpect(status().isOk()) - .andDo(document("tags-list-example") - .withResponseFields( - fieldWithPath("_embedded.tags").description("An array of <>"))); + .andDo(document("tags-list-example", + responseFields( + fieldWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -233,9 +236,9 @@ public void tagsCreateExample() throws Exception { post("/tags").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()) - .andDo(document("tags-create-example") - .withRequestFields( - fieldWithPath("name").description("The name of the tag"))); + .andDo(document("tags-create-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); } @Test @@ -274,12 +277,11 @@ public void noteUpdateExample() throws Exception { patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(noteUpdate))) .andExpect(status().isNoContent()) - .andDo(document("note-update-example") - .withRequestFields( + .andDo(document("note-update-example", + requestFields( 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())); - + fieldWithPath("tags").description("An array of tag resource URIs").optional()))); } @Test @@ -297,13 +299,13 @@ public void tagGetExample() throws Exception { this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example") - .withLinks( + .andDo(document("tag-get-example", + links( linkWithRel("self").description("This <>"), - linkWithRel("notes").description("The <> that have this tag")) - .withResponseFields( + linkWithRel("notes").description("The <> that have this tag")), + responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources"))); + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -325,9 +327,9 @@ public void tagUpdateExample() throws Exception { patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tagUpdate))) .andExpect(status().isNoContent()) - .andDo(document("tag-update-example") - .withRequestFields( - fieldWithPath("name").description("The name of the tag"))); + .andDo(document("tag-update-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); } private void createNote(String title, String body) { 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 1901ea85d..c8c7dca22 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 @@ -21,7 +21,10 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; @@ -89,25 +92,25 @@ public void errorExample() throws Exception { .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example") - .withResponseFields( + .andDo(document("error-example", + responseFields( 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"))); + 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( + .andDo(document("index-example", + links( linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")) - .withResponseFields( - fieldWithPath("_links").description("<> to other resources"))); + linkWithRel("tags").description("The <>")), + responseFields( + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -122,9 +125,9 @@ public void notesListExample() throws Exception { this.mockMvc.perform(get("/notes")) .andExpect(status().isOk()) - .andDo(document("notes-list-example") - .withResponseFields( - fieldWithPath("_embedded.notes").description("An array of <>"))); + .andDo(document("notes-list-example", + responseFields( + fieldWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -148,11 +151,11 @@ public void notesCreateExample() throws Exception { post("/notes").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) - .andDo(document("notes-create-example") - .withRequestFields( + .andDo(document("notes-create-example", + requestFields( 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"))); + fieldWithPath("tags").description("An array of tag resource URIs")))); } @Test @@ -185,14 +188,14 @@ public void noteGetExample() throws Exception { .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( + .andDo(document("note-get-example", + links( linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")) - .withResponseFields( + linkWithRel("note-tags").description("This note's <>")), + responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources"))); + fieldWithPath("_links").description("<> to other resources")))); } @@ -207,9 +210,9 @@ public void tagsListExample() throws Exception { this.mockMvc.perform(get("/tags")) .andExpect(status().isOk()) - .andDo(document("tags-list-example") - .withResponseFields( - fieldWithPath("_embedded.tags").description("An array of <>"))); + .andDo(document("tags-list-example", + responseFields( + fieldWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -221,9 +224,9 @@ public void tagsCreateExample() throws Exception { post("/tags").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()) - .andDo(document("tags-create-example") - .withRequestFields( - fieldWithPath("name").description("The name of the tag"))); + .andDo(document("tags-create-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); } @Test @@ -262,11 +265,11 @@ public void noteUpdateExample() throws Exception { patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(noteUpdate))) .andExpect(status().isNoContent()) - .andDo(document("note-update-example") - .withRequestFields( + .andDo(document("note-update-example", + requestFields( 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())); + fieldWithPath("tags").description("An array of tag resource URIs").optional()))); } @Test @@ -284,13 +287,13 @@ public void tagGetExample() throws Exception { this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example") - .withLinks( + .andDo(document("tag-get-example", + links( linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")) - .withResponseFields( + linkWithRel("tagged-notes").description("The <> that have this tag")), + responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources"))); + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -312,9 +315,9 @@ public void tagUpdateExample() throws Exception { patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tagUpdate))) .andExpect(status().isNoContent()) - .andDo(document("tag-update-example") - .withRequestFields( - fieldWithPath("name").description("The name of the tag"))); + .andDo(document("tag-update-example", + requestFields( + 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/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java index e2002904a..a72ded801 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java @@ -27,6 +27,7 @@ import org.springframework.core.BridgeMethodResolver; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.response.ResponsePostProcessor; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.ReflectionUtils; @@ -47,19 +48,22 @@ public final class ResponseModifier { /** * Provides a {@link RestDocumentationResultHandler} that can be used to document the - * request and modified result. - * @param identifier An identifier for the API call that is being documented + * request and modified response. + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets to use to document the call * @return the result handler that will produce the documentation */ - public RestDocumentationResultHandler andDocument(String identifier) { - return new ResponseModifyingRestDocumentationResultHandler(identifier); + public RestDocumentationResultHandler andDocument(String identifier, + Snippet... snippets) { + return new ResponseModifyingRestDocumentationResultHandler(identifier, snippets); } class ResponseModifyingRestDocumentationResultHandler extends RestDocumentationResultHandler { - public ResponseModifyingRestDocumentationResultHandler(String identifier) { - super(identifier); + private ResponseModifyingRestDocumentationResultHandler(String identifier, + Snippet... snippets) { + super(identifier, snippets); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index 58f279d1b..d5977b3c9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -19,6 +19,7 @@ import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.restdocs.response.ResponsePostProcessor; import org.springframework.restdocs.response.ResponsePostProcessors; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -39,6 +40,7 @@ private RestDocumentation() { /** * Provides access to a {@link MockMvcConfigurer} that can be used to configure the * REST documentation when building a {@link MockMvc} instance. + * * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) * @return the configurer */ @@ -47,15 +49,18 @@ public static RestDocumentationConfigurer documentationConfiguration() { } /** - * Documents the API call using the given {@code identifier}. + * Documents the API call with the given {@code identifier} using the given + * {@code handlers}. * - * @param identifier An identifier for the API call that is being documented + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets that will document the API call * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) */ - public static RestDocumentationResultHandler document(String identifier) { - return new RestDocumentationResultHandler(identifier); + public static RestDocumentationResultHandler document(String identifier, + Snippet... snippets) { + return new RestDocumentationResultHandler(identifier, snippets); } /** diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java index e702fdafb..160006d40 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java @@ -19,6 +19,7 @@ import java.net.URI; import org.springframework.http.HttpMethod; +import org.springframework.restdocs.request.RequestDocumentation; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -26,13 +27,13 @@ /** * A drop-in replacement for {@link MockMvcRequestBuilders} that captures a request's URL * template and makes it available for documentation. Required when - * {@link RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs .request.ParameterDescriptor...) - * documenting path parameters} and recommended for general usage. + * {@link RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...) + * ) documenting path parameters} and recommended for general usage. * * @author Andy Wilkinson * @see MockMvcRequestBuilders - * @see RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs.request.ParameterDescriptor...) - * @see RestDocumentationResultHandler#withPathParameters(java.util.Map, + * @see RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...) + * @see RequestDocumentation#pathParameters(java.util.Map, * org.springframework.restdocs.request.ParameterDescriptor...) */ public abstract class RestDocumentationRequestBuilders { 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 c3930548c..68d595557 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -16,28 +16,11 @@ package org.springframework.restdocs; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; -import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; -import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; -import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; -import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; - import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.Map; -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.payload.FieldDescriptor; -import org.springframework.restdocs.payload.PayloadDocumentation; -import org.springframework.restdocs.request.ParameterDescriptor; -import org.springframework.restdocs.request.RequestDocumentation; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -46,317 +29,32 @@ * * @author Andy Wilkinson * @author Andreas Evers - * @see RestDocumentation#document(String) + * @see RestDocumentation#document(String, Snippet...) */ public class RestDocumentationResultHandler implements ResultHandler { private final String identifier; - private SnippetWritingResultHandler curlRequest; - - private SnippetWritingResultHandler httpRequest; - - private SnippetWritingResultHandler httpResponse; + private final List snippets; - private List delegates = new ArrayList<>(); - - RestDocumentationResultHandler(String identifier) { + RestDocumentationResultHandler(String identifier, Snippet... snippets) { this.identifier = identifier; - this.curlRequest = documentCurlRequest(this.identifier, null); - this.httpRequest = documentHttpRequest(this.identifier, null); - this.httpResponse = documentHttpResponse(this.identifier, null); - } - - /** - * Customizes the default curl request snippet generation to make the given attributes - * available. - * - * @param attributes the attributes - * @return {@code this} - */ - public RestDocumentationResultHandler withCurlRequest(Map attributes) { - this.curlRequest = documentCurlRequest(this.identifier, attributes); - return this; - } - - /** - * Customizes the default HTTP request snippet generation to make the given attributes - * available. - * - * @param attributes the attributes - * @return {@code this} - */ - public RestDocumentationResultHandler withHttpRequest(Map attributes) { - this.httpRequest = documentHttpRequest(this.identifier, attributes); - return this; - } - - /** - * Customizes the default HTTP response snippet generation to make the given - * attributes available. - * - * @param attributes the attributes - * @return {@code this} - */ - public RestDocumentationResultHandler withHttpResponse(Map attributes) { - this.httpResponse = documentHttpResponse(this.identifier, attributes); - return this; - } - - /** - * Document the links in the response using the given {@code descriptors}. The links - * are extracted from the response based on its content type. - *

- * If a link 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 link is - * described but is not present in the response a failure will also occur when this - * handler is invoked. - * - * @param descriptors the link descriptors - * @return {@code this} - * @see HypermediaDocumentation#linkWithRel(String) - * @see LinkExtractors#extractorForContentType(String) - */ - public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) { - return withLinks(null, null, descriptors); - } - - /** - * Document the links in the response using the given {@code descriptors}. The links - * are extracted from the response using the given {@code linkExtractor}. - *

- * If a link 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 link is - * described but is not present in the response a failure will also occur when this - * handler is invoked. - * - * @param linkExtractor used to extract the links from the response - * @param descriptors the link descriptors - * @return {@code this} - * @see HypermediaDocumentation#linkWithRel(String) - */ - public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor, - LinkDescriptor... descriptors) { - return this.withLinks(null, linkExtractor, descriptors); - } - - /** - * Document the links in the response using the given {@code descriptors}. The links - * are extracted from the response based on its content type. The given - * {@code attributes} are made available during the generation of the links snippet. - *

- * If a link 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 link is - * described but is not present in the response a failure will also occur when this - * handler is invoked. - * - * @param attributes the attributes - * @param descriptors the link descriptors - * @return {@code this} - * @see HypermediaDocumentation#linkWithRel(String) - * @see LinkExtractors#extractorForContentType(String) - */ - public RestDocumentationResultHandler withLinks(Map attributes, - LinkDescriptor... descriptors) { - return withLinks(attributes, null, descriptors); - } - - /** - * Document the links in the response using the given {@code descriptors}. The links - * are extracted from the response using the given {@code linkExtractor}. The given - * {@code attributes} are made available during the generation of the links snippet. - *

- * If a link 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 link is - * described but is not present in the response a failure will also occur when this - * handler is invoked. - * - * @param attributes the attributes - * @param linkExtractor used to extract the links from the response - * @param descriptors the link descriptors - * @return {@code this} - * @see HypermediaDocumentation#linkWithRel(String) - */ - public RestDocumentationResultHandler withLinks(Map attributes, - LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - this.delegates.add(documentLinks(this.identifier, attributes, linkExtractor, - descriptors)); - return this; - } - - /** - * Document the fields in the request using the given {@code descriptors}. - *

- * If a field is present in the request but is not documented by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request a - * failure will also occur. For payloads with a hierarchical structure, documenting a - * field is sufficient for all of its descendants to also be treated as having been - * documented. - * - * @param descriptors the link descriptors - * @return {@code this} - * @see PayloadDocumentation#fieldWithPath(String) - */ - public RestDocumentationResultHandler withRequestFields( - FieldDescriptor... descriptors) { - return this.withRequestFields(null, descriptors); - } - - /** - * Document the fields in the request using the given {@code descriptors}. The given - * {@code attributes} are made available during the generation of the request fields - * snippet. - *

- * If a field is present in the request but is not documented by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request a - * failure will also occur. For payloads with a hierarchical structure, documenting a - * field is sufficient for all of its descendants to also be treated as having been - * documented. - * - * @param descriptors the link descriptors - * @param attributes the attributes - * @return {@code this} - * @see PayloadDocumentation#fieldWithPath(String) - */ - public RestDocumentationResultHandler withRequestFields( - Map attributes, FieldDescriptor... descriptors) { - this.delegates - .add(documentRequestFields(this.identifier, attributes, descriptors)); - return this; - } - - /** - * Document the fields in the response using the given {@code descriptors}. - *

- * If a field is present in the response but is not documented by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - * - * @param descriptors the link descriptors - * @return {@code this} - * @see PayloadDocumentation#fieldWithPath(String) - */ - public RestDocumentationResultHandler withResponseFields( - FieldDescriptor... descriptors) { - return this.withResponseFields(null, descriptors); - } - - /** - * Document the fields in the response using the given {@code descriptors}. The given - * {@code attributes} are made available during the generation of the request fields - * snippet. - *

- * If a field is present in the response but is not documented by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - * - * @param descriptors the link descriptors - * @param attributes the attributes - * @return {@code this} - * @see PayloadDocumentation#fieldWithPath(String) - */ - public RestDocumentationResultHandler withResponseFields( - Map attributes, FieldDescriptor... descriptors) { - this.delegates.add(documentResponseFields(this.identifier, attributes, - descriptors)); - return this; - } - - /** - * Documents the parameters in the request's query string using the given - * {@code descriptors}. - *

- * If a parameter is present in the query string but is not described by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * parameter is described but is not present in the request a failure will also occur - * when this handler is invoked. - * - * @param descriptors the parameter descriptors - * @return {@code this} - * @see RequestDocumentation#parameterWithName(String) - */ - public RestDocumentationResultHandler withQueryParameters( - ParameterDescriptor... descriptors) { - return this.withQueryParameters(null, descriptors); - } - - /** - * Documents the parameters in the request's query string using the given - * {@code descriptors}. The given {@code attributes} are made available during the - * generation of the query parameters snippet. - *

- * If a parameter is present in the query string but is not described by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * parameter is described but is not present in the request a failure will also occur - * when this handler is invoked. - * - * @param descriptors the parameter descriptors - * @param attributes the attributes - * @return {@code this} - * @see RequestDocumentation#parameterWithName(String) - */ - public RestDocumentationResultHandler withQueryParameters( - Map attributes, ParameterDescriptor... descriptors) { - this.delegates.add(documentQueryParameters(this.identifier, attributes, - descriptors)); - return this; - } - - /** - * Documents the parameters in the request's path using the given {@code descriptors}. - *

- * If a parameter is present in the path but is not described by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * parameter is described but is not present in the request a failure will also occur - * when this handler is invoked. - * - * @param descriptors the parameter descriptors - * @return {@code this} - * @see RequestDocumentation#parameterWithName(String) - */ - public RestDocumentationResultHandler withPathParameters( - ParameterDescriptor... descriptors) { - return this.withPathParameters(null, descriptors); - } - - /** - * Documents the parameters in the request's path using the given {@code descriptors}. - * The given {@code attributes} are made available during the generation of the path - * parameters snippet. - *

- * If a parameter is present in the path but is not described by one of the - * descriptors a failure will occur when this handler is invoked. Similarly, if a - * parameter is described but is not present in the request a failure will also occur - * when this handler is invoked. - * - * @param descriptors the parameter descriptors - * @param attributes the attributes - * @return {@code this} - * @see RequestDocumentation#parameterWithName(String) - */ - public RestDocumentationResultHandler withPathParameters( - Map attributes, ParameterDescriptor... descriptors) { - this.delegates.add(documentPathParameters(this.identifier, attributes, - descriptors)); - return this; + this.snippets = Arrays.asList(snippets); } @Override public void handle(MvcResult result) throws Exception { - this.curlRequest.handle(result); - this.httpRequest.handle(result); - this.httpResponse.handle(result); - for (ResultHandler delegate : this.delegates) { - delegate.handle(result); + for (Snippet snippet : getSnippets(result)) { + snippet.document(this.identifier, result); } } + @SuppressWarnings("unchecked") + private List getSnippets(MvcResult result) { + List combinedSnippets = new ArrayList<>((List) result.getRequest() + .getAttribute("org.springframework.restdocs.defaultSnippets")); + combinedSnippets.addAll(this.snippets); + return combinedSnippets; + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index a8120c525..f84530585 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -27,7 +27,7 @@ abstract class AbstractConfigurer { /** - * Applies the configuration, possibly be modifying the given {@code request} + * Applies the configuration, possibly by modifying the given {@code request} * @param request the request that may be modified */ abstract void apply(MockHttpServletRequest request); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index ba9304208..2f9417b90 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -49,9 +49,9 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { private final RequestPostProcessor requestPostProcessor; - private TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); - private WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); + private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); /** * Creates a new {@link RestDocumentationConfigurer}. @@ -65,19 +65,44 @@ Arrays. asList(this.uriConfigurer, this.templateEngineConfigurer)); } + /** + * Returns a {@link UriConfigurer} that can be used to configure the request URIs that + * will be documented. + * + * @return the URI configurer + */ public UriConfigurer uris() { return this.uriConfigurer; } + /** + * Returns a {@link SnippetConfigurer} that can be used to configure the snippets that + * will be generated. + * + * @return the snippet configurer + */ public SnippetConfigurer snippets() { return this.snippetConfigurer; } + /** + * Configures the {@link TemplateEngine} that will be used for snippet rendering. + * + * @param templateEngine the template engine to use + * @return {@code this} + */ public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) { this.templateEngineConfigurer.setTemplateEngine(templateEngine); return this; } + /** + * Configures the {@link WriterResolver} that will be used to resolve a writer for a + * snippet. + * + * @param writerResolver The writer resolver to use + * @return {@code this} + */ public RestDocumentationConfigurer writerResolver(WriterResolver writerResolver) { this.writerResolverConfigurer.setWriterResolver(writerResolver); return this; @@ -175,4 +200,5 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) } } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java index f5b3f2dcf..2a95a7f18 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java @@ -33,10 +33,12 @@ public final class RestDocumentationContext { private final TestContext testContext; - public RestDocumentationContext() { - this(null); - } - + /** + * Creates a new {@code RestDocumentationContext} backed by the given + * {@code testContext}. + * + * @param testContext the test context + */ public RestDocumentationContext(TestContext testContext) { this.testContext = testContext; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java index 337caf862..6a9645cdc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java @@ -46,6 +46,12 @@ public class RestDocumentationContextPlaceholderResolver implements PlaceholderR private final RestDocumentationContext context; + /** + * Creates a new placeholder resolver that will resolve placeholders using the given + * {@code context}. + * + * @param context the context to use + */ public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) { this.context = context; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index ae992f7fe..992086f73 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -16,7 +16,13 @@ package org.springframework.restdocs.config; +import java.util.Arrays; +import java.util.List; + import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.http.HttpDocumentation; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.WriterResolver; /** @@ -28,6 +34,10 @@ public class SnippetConfigurer extends AbstractNestedConfigurer { + private List defaultSnippets = Arrays.asList( + CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse()); + /** * The default encoding for documentation snippets * @see #withEncoding(String) @@ -43,7 +53,7 @@ public class SnippetConfigurer extends /** * Configures any documentation snippets to be written using the given * {@code encoding}. The default is UTF-8. - * @param encoding The encoding + * @param encoding the encoding * @return {@code this} */ public SnippetConfigurer withEncoding(String encoding) { @@ -55,5 +65,18 @@ public SnippetConfigurer withEncoding(String encoding) { void apply(MockHttpServletRequest request) { ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) .setEncoding(this.snippetEncoding); + request.setAttribute("org.springframework.restdocs.defaultSnippets", + this.defaultSnippets); + } + + /** + * Configures the documentation snippets that will be produced by default. + * + * @param defaultSnippets the default snippets + * @return {@code this} + */ + public SnippetConfigurer withDefaults(Snippet... defaultSnippets) { + this.defaultSnippets = Arrays.asList(defaultSnippets); + return this; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 12b40b7e6..be692cc93 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -16,19 +16,9 @@ package org.springframework.restdocs.curl; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting a RESTful API as if it were being driven using @@ -46,151 +36,25 @@ private CurlDocumentation() { } /** - * Produces a documentation snippet containing the request formatted as a cURL command + * Returns a handler that will produce a snippet containing the curl request for the + * API call. * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the curl request - * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentCurlRequest(String identifier, - Map attributes) { - return new CurlRequestWritingResultHandler(identifier, attributes); + public static Snippet curlRequest() { + return new CurlRequestSnippet(); } - private static final class CurlRequestWritingResultHandler extends - SnippetWritingResultHandler { - - private static final String SCHEME_HTTP = "http"; - - private static final String SCHEME_HTTPS = "https"; - - private static final int STANDARD_PORT_HTTP = 80; - - private static final int STANDARD_PORT_HTTPS = 443; - - private CurlRequestWritingResultHandler(String identifier, - Map attributes) { - super(identifier, "curl-request", attributes); - } - - @Override - public Map doHandle(MvcResult result) throws IOException { - Map model = new HashMap(); - model.put("arguments", getCurlCommandArguments(result)); - return model; - } - - private String getCurlCommandArguments(MvcResult result) throws IOException { - StringWriter command = new StringWriter(); - PrintWriter printer = new PrintWriter(command); - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - result.getRequest()); - - printer.print("'"); - writeAuthority(request, printer); - writePathAndQueryString(request, printer); - printer.print("'"); - - writeOptionToIncludeHeadersInOutput(printer); - writeHttpMethodIfNecessary(request, printer); - writeHeaders(request, printer); - - if (request.isMultipartRequest()) { - writeParts(request, printer); - } - - writeContent(request, printer); - - return command.toString(); - } - - private void writeAuthority(DocumentableHttpServletRequest request, - PrintWriter writer) { - writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); - - if (isNonStandardPort(request)) { - writer.print(String.format(":%d", request.getPort())); - } - } - - private boolean isNonStandardPort(DocumentableHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); - } - - private void writePathAndQueryString(DocumentableHttpServletRequest request, - PrintWriter writer) { - if (StringUtils.hasText(request.getContextPath())) { - writer.print(String.format( - request.getContextPath().startsWith("/") ? "%s" : "/%s", - request.getContextPath())); - } - - writer.print(request.getRequestUriWithQueryString()); - } - - private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { - writer.print(" -i"); - } - - private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request, - PrintWriter writer) { - if (!request.isGetRequest()) { - writer.print(String.format(" -X %s", request.getMethod())); - } - } - - private void writeHeaders(DocumentableHttpServletRequest request, - PrintWriter writer) { - for (Entry> entry : request.getHeaders().entrySet()) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } - } - } - - private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) - throws IOException { - for (Entry> entry : request.getMultipartFiles() - .entrySet()) { - for (MultipartFile file : entry.getValue()) { - writer.printf(" -F '%s=", file.getName()); - if (!StringUtils.hasText(file.getOriginalFilename())) { - writer.append(new String(file.getBytes())); - } - else { - writer.printf("@%s", file.getOriginalFilename()); - } - - if (StringUtils.hasText(file.getContentType())) { - writer.append(";type=").append(file.getContentType()); - } - writer.append("'"); - } - } - - } - - private void writeContent(DocumentableHttpServletRequest request, - PrintWriter writer) throws IOException { - if (request.getContentLength() > 0) { - writer.print(String.format(" -d '%s'", request.getContentAsString())); - } - else if (request.isMultipartRequest()) { - for (Entry entry : request.getParameterMap().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (request.isPostRequest() || request.isPutRequest()) { - String queryString = request.getParameterMapAsQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); - } - } - } + /** + * Returns a handler that will produce a snippet containing the curl request for the + * API call. The given {@code attributes} will be available during snippet generation. + * + * @param attributes Attributes made available during rendering of the curl request + * snippet + * @return the handler that will produce the snippet + */ + public static Snippet curlRequest(Map attributes) { + return new CurlRequestSnippet(attributes); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java new file mode 100644 index 000000000..a1e5e8fd5 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -0,0 +1,171 @@ +/* + * 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.curl; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * A {@link Snippet} that documents the curl command for a request. + * + * @author Andy Wilkinson + */ +class CurlRequestSnippet extends TemplatedSnippet { + + private static final String SCHEME_HTTP = "http"; + + private static final String SCHEME_HTTPS = "https"; + + private static final int STANDARD_PORT_HTTP = 80; + + private static final int STANDARD_PORT_HTTPS = 443; + + CurlRequestSnippet() { + this(null); + } + + CurlRequestSnippet(Map attributes) { + super("curl-request", attributes); + } + + @Override + public Map document(MvcResult result) throws IOException { + Map model = new HashMap(); + model.put("arguments", getCurlCommandArguments(result)); + return model; + } + + private String getCurlCommandArguments(MvcResult result) throws IOException { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + result.getRequest()); + + printer.print("'"); + writeAuthority(request, printer); + writePathAndQueryString(request, printer); + printer.print("'"); + + writeOptionToIncludeHeadersInOutput(printer); + writeHttpMethodIfNecessary(request, printer); + writeHeaders(request, printer); + + if (request.isMultipartRequest()) { + writeParts(request, printer); + } + + writeContent(request, printer); + + return command.toString(); + } + + private void writeAuthority(DocumentableHttpServletRequest request, PrintWriter writer) { + writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); + + if (isNonStandardPort(request)) { + writer.print(String.format(":%d", request.getPort())); + } + } + + private boolean isNonStandardPort(DocumentableHttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); + } + + private void writePathAndQueryString(DocumentableHttpServletRequest request, + PrintWriter writer) { + if (StringUtils.hasText(request.getContextPath())) { + writer.print(String.format(request.getContextPath().startsWith("/") ? "%s" + : "/%s", request.getContextPath())); + } + + writer.print(request.getRequestUriWithQueryString()); + } + + private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { + writer.print(" -i"); + } + + private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request, + PrintWriter writer) { + if (!request.isGetRequest()) { + writer.print(String.format(" -X %s", request.getMethod())); + } + } + + private void writeHeaders(DocumentableHttpServletRequest request, PrintWriter writer) { + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } + } + } + + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) + throws IOException { + for (Entry> entry : request.getMultipartFiles() + .entrySet()) { + for (MultipartFile file : entry.getValue()) { + writer.printf(" -F '%s=", file.getName()); + if (!StringUtils.hasText(file.getOriginalFilename())) { + writer.append(new String(file.getBytes())); + } + else { + writer.printf("@%s", file.getOriginalFilename()); + } + + if (StringUtils.hasText(file.getContentType())) { + writer.append(";type=").append(file.getContentType()); + } + writer.append("'"); + } + } + + } + + private void writeContent(DocumentableHttpServletRequest request, PrintWriter writer) + throws IOException { + if (request.getContentLength() > 0) { + writer.print(String.format(" -d '%s'", request.getContentAsString())); + } + else if (request.isMultipartRequest()) { + for (Entry entry : request.getParameterMap().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); + } + } + } + else if (request.isPostRequest() || request.isPutRequest()) { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index b24618bb3..c097e40e3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -16,23 +16,9 @@ package org.springframework.restdocs.http; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting a RESTful API's HTTP requests. @@ -42,213 +28,49 @@ */ public abstract class HttpDocumentation { - private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - private HttpDocumentation() { } /** - * Produces a documentation snippet containing the request formatted as an HTTP - * request + * Returns a handler that will produce a snippet containing the HTTP request for the + * API call. * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the HTTP requst - * snippet * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpRequest(String identifier, - Map attributes) { - return new HttpRequestWritingResultHandler(identifier, attributes); + public static Snippet httpRequest() { + return new HttpRequestSnippet(); } /** - * Produces a documentation snippet containing the response formatted as the HTTP - * response sent by the server - * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the HTTP response - * snippet + * Returns a handler that will produce a snippet containing the HTTP request for the + * API call. The given {@code attributes} will be available during snippet generation. + * + * @param attributes the attributes * @return the handler that will produce the snippet */ - public static SnippetWritingResultHandler documentHttpResponse(String identifier, - Map attributes) { - return new HttpResponseWritingResultHandler(identifier, attributes); - + public static Snippet httpRequest(Map attributes) { + return new HttpRequestSnippet(attributes); } - private static final class HttpRequestWritingResultHandler extends - SnippetWritingResultHandler { - - private HttpRequestWritingResultHandler(String identifier, - Map attributes) { - super(identifier, "http-request", attributes); - } - - @Override - public Map doHandle(MvcResult result) throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - result.getRequest()); - Map model = new HashMap(); - model.put("method", result.getRequest().getMethod()); - model.put("path", request.getRequestUriWithQueryString()); - model.put("headers", getHeaders(request)); - model.put("requestBody", getRequestBody(request)); - return model; - } - - private List> getHeaders( - DocumentableHttpServletRequest request) { - List> headers = new ArrayList<>(); - if (requiresHostHeader(request)) { - headers.add(header(HttpHeaders.HOST, request.getHost())); - } - - for (Entry> header : request.getHeaders().entrySet()) { - for (String value : header.getValue()) { - if (header.getKey() == HttpHeaders.CONTENT_TYPE - && request.isMultipartRequest()) { - headers.add(header(header.getKey(), String.format( - "%s; boundary=%s", value, MULTIPART_BOUNDARY))); - } - else { - headers.add(header(header.getKey(), value)); - } - - } - } - if (requiresFormEncodingContentType(request)) { - headers.add(header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE)); - } - return headers; - } - - private String getRequestBody(DocumentableHttpServletRequest request) - throws IOException { - StringWriter httpRequest = new StringWriter(); - PrintWriter writer = new PrintWriter(httpRequest); - if (request.getContentLength() > 0) { - writer.println(); - writer.print(request.getContentAsString()); - } - else if (request.isPostRequest() || request.isPutRequest()) { - if (request.isMultipartRequest()) { - writeParts(request, writer); - } - else { - String queryString = request.getParameterMapAsQueryString(); - if (StringUtils.hasText(queryString)) { - writer.println(); - writer.print(queryString); - } - } - } - return httpRequest.toString(); - } - - private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) - throws IOException { - writer.println(); - for (Entry parameter : request.getParameterMap().entrySet()) { - for (String value : parameter.getValue()) { - writePartBoundary(writer); - writePart(parameter.getKey(), value, null, writer); - writer.println(); - } - } - for (Entry> entry : request.getMultipartFiles() - .entrySet()) { - for (MultipartFile file : entry.getValue()) { - writePartBoundary(writer); - writePart(file, writer); - writer.println(); - } - } - writeMultipartEnd(writer); - } - - private void writePartBoundary(PrintWriter writer) { - writer.printf("--%s%n", MULTIPART_BOUNDARY); - } - - private void writePart(String name, String value, String contentType, - PrintWriter writer) { - writer.printf("Content-Disposition: form-data; name=%s%n", name); - if (StringUtils.hasText(contentType)) { - writer.printf("Content-Type: %s%n", contentType); - } - writer.println(); - writer.print(value); - } - - private void writePart(MultipartFile part, PrintWriter writer) throws IOException { - writePart(part.getName(), new String(part.getBytes()), part.getContentType(), - writer); - } - - private void writeMultipartEnd(PrintWriter writer) { - writer.printf("--%s--", MULTIPART_BOUNDARY); - } - - private boolean requiresHostHeader(DocumentableHttpServletRequest request) { - return request.getHeaders().get(HttpHeaders.HOST) == null; - } - - private boolean requiresFormEncodingContentType( - DocumentableHttpServletRequest request) { - return request.getHeaders().getContentType() == null - && (request.isPostRequest() || request.isPutRequest()) - && StringUtils.hasText(request.getParameterMapAsQueryString()); - } - - private Map header(String name, String value) { - Map header = new HashMap<>(); - header.put("name", name); - header.put("value", value); - return header; - } + /** + * Returns a handler that will produce a snippet containing the HTTP response for the + * API call. + * @return the handler that will produce the snippet + */ + public static Snippet httpResponse() { + return new HttpResponseSnippet(); } - private static final class HttpResponseWritingResultHandler extends - SnippetWritingResultHandler { - - private HttpResponseWritingResultHandler(String identifier, - Map attributes) { - super(identifier, "http-response", attributes); - } - - @Override - public Map doHandle(MvcResult result) throws IOException { - HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); - Map model = new HashMap(); - model.put( - "responseBody", - StringUtils.hasLength(result.getResponse().getContentAsString()) ? String - .format("%n%s", result.getResponse().getContentAsString()) - : ""); - model.put("statusCode", status.value()); - model.put("statusReason", status.getReasonPhrase()); - model.put("headers", headers(result)); - return model; - } - - private List> headers(MvcResult result) { - List> headers = new ArrayList<>(); - for (String headerName : result.getResponse().getHeaderNames()) { - for (String header : result.getResponse().getHeaders(headerName)) { - headers.add(header(headerName, header)); - } - } - return headers; - } - - private Map header(String name, String value) { - Map header = new HashMap<>(); - header.put("name", name); - header.put("value", value); - return header; - } + /** + * Returns a handler that will produce a snippet containing the HTTP response for the + * API call. The given {@code attributes} will be available during snippet generation. + * + * @param attributes the attributes + * @return the handler that will produce the snippet + */ + public static Snippet httpResponse(Map attributes) { + return new HttpResponseSnippet(attributes); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java new file mode 100644 index 000000000..7ff397566 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -0,0 +1,175 @@ +/* + * 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.http; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * A {@link Snippet} that documents an HTTP request. + * + * @author Andy Wilkinson + */ +class HttpRequestSnippet extends TemplatedSnippet { + + private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + + HttpRequestSnippet() { + this(null); + } + + HttpRequestSnippet(Map attributes) { + super("http-request", attributes); + } + + @Override + public Map document(MvcResult result) throws IOException { + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + result.getRequest()); + Map model = new HashMap(); + model.put("method", result.getRequest().getMethod()); + model.put("path", request.getRequestUriWithQueryString()); + model.put("headers", getHeaders(request)); + model.put("requestBody", getRequestBody(request)); + return model; + } + + private List> getHeaders(DocumentableHttpServletRequest request) { + List> headers = new ArrayList<>(); + if (requiresHostHeader(request)) { + headers.add(header(HttpHeaders.HOST, request.getHost())); + } + + for (Entry> header : request.getHeaders().entrySet()) { + for (String value : header.getValue()) { + if (header.getKey() == HttpHeaders.CONTENT_TYPE + && request.isMultipartRequest()) { + headers.add(header(header.getKey(), + String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); + } + else { + headers.add(header(header.getKey(), value)); + } + + } + } + if (requiresFormEncodingContentType(request)) { + headers.add(header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + } + return headers; + } + + private String getRequestBody(DocumentableHttpServletRequest request) + throws IOException { + StringWriter httpRequest = new StringWriter(); + PrintWriter writer = new PrintWriter(httpRequest); + if (request.getContentLength() > 0) { + writer.println(); + writer.print(request.getContentAsString()); + } + else if (request.isPostRequest() || request.isPutRequest()) { + if (request.isMultipartRequest()) { + writeParts(request, writer); + } + else { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + writer.println(); + writer.print(queryString); + } + } + } + return httpRequest.toString(); + } + + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) + throws IOException { + writer.println(); + for (Entry parameter : request.getParameterMap().entrySet()) { + for (String value : parameter.getValue()) { + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, writer); + writer.println(); + } + } + for (Entry> entry : request.getMultipartFiles() + .entrySet()) { + for (MultipartFile file : entry.getValue()) { + writePartBoundary(writer); + writePart(file, writer); + writer.println(); + } + } + writeMultipartEnd(writer); + } + + private void writePartBoundary(PrintWriter writer) { + writer.printf("--%s%n", MULTIPART_BOUNDARY); + } + + private void writePart(String name, String value, String contentType, + PrintWriter writer) { + writer.printf("Content-Disposition: form-data; name=%s%n", name); + if (StringUtils.hasText(contentType)) { + writer.printf("Content-Type: %s%n", contentType); + } + writer.println(); + writer.print(value); + } + + private void writePart(MultipartFile part, PrintWriter writer) throws IOException { + writePart(part.getName(), new String(part.getBytes()), part.getContentType(), + writer); + } + + private void writeMultipartEnd(PrintWriter writer) { + writer.printf("--%s--", MULTIPART_BOUNDARY); + } + + private boolean requiresHostHeader(DocumentableHttpServletRequest request) { + return request.getHeaders().get(HttpHeaders.HOST) == null; + } + + private boolean requiresFormEncodingContentType(DocumentableHttpServletRequest request) { + return request.getHeaders().getContentType() == null + && (request.isPostRequest() || request.isPutRequest()) + && StringUtils.hasText(request.getParameterMapAsQueryString()); + } + + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java new file mode 100644 index 000000000..b3dfb3c24 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -0,0 +1,76 @@ +/* + * 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.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents an HTTP response. + * + * @author Andy Wilkinson + */ +class HttpResponseSnippet extends TemplatedSnippet { + + HttpResponseSnippet() { + this(null); + } + + HttpResponseSnippet(Map attributes) { + super("http-response", attributes); + } + + @Override + public Map document(MvcResult result) throws IOException { + HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); + Map model = new HashMap(); + model.put( + "responseBody", + StringUtils.hasLength(result.getResponse().getContentAsString()) ? String + .format("%n%s", result.getResponse().getContentAsString()) : ""); + model.put("statusCode", status.value()); + model.put("statusReason", status.getReasonPhrase()); + model.put("headers", headers(result)); + return model; + } + + private List> headers(MvcResult result) { + List> headers = new ArrayList<>(); + for (String headerName : result.getResponse().getHeaderNames()) { + for (String header : result.getResponse().getHeaders(headerName)) { + headers.add(header(headerName, header)); + } + } + return headers; + } + + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java new file mode 100644 index 000000000..45c73c234 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -0,0 +1,46 @@ +/* + * 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.hypermedia; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.springframework.mock.web.MockHttpServletResponse; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Abstract base class for a {@link LinkExtractor} that extracts links from JSON + * + * @author Andy Wilkinson + */ +abstract class AbstractJsonLinkExtractor implements LinkExtractor { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + @SuppressWarnings("unchecked") + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + Map jsonContent = this.objectMapper.readValue( + response.getContentAsString(), Map.class); + return extractLinks(jsonContent); + } + + protected abstract Map> extractLinks(Map json); +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java new file mode 100644 index 000000000..e7bc955f6 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -0,0 +1,65 @@ +/* + * 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.hypermedia; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * {@link LinkExtractor} that extracts links in Atom format. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("unchecked") +class AtomLinkExtractor extends AbstractJsonLinkExtractor { + + @Override + public Map> extractLinks(Map json) { + MultiValueMap extractedLinks = new LinkedMultiValueMap<>(); + Object possibleLinks = json.get("links"); + if (possibleLinks instanceof Collection) { + Collection linksCollection = (Collection) possibleLinks; + for (Object linkObject : linksCollection) { + if (linkObject instanceof Map) { + Link link = maybeCreateLink((Map) linkObject); + maybeStoreLink(link, extractedLinks); + } + } + } + return extractedLinks; + } + + private static Link maybeCreateLink(Map linkMap) { + Object hrefObject = linkMap.get("href"); + Object relObject = linkMap.get("rel"); + if (relObject instanceof String && hrefObject instanceof String) { + return new Link((String) relObject, (String) hrefObject); + } + return null; + } + + private static void maybeStoreLink(Link link, + MultiValueMap extractedLinks) { + if (link != null) { + extractedLinks.add(link.getRel(), link); + } + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java new file mode 100644 index 000000000..c114d138e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -0,0 +1,58 @@ +package org.springframework.restdocs.hypermedia; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.StringUtils; + +/** + * {@link LinkExtractor} that delegates to other link extractors based on the response's + * content type. + * + * @author Andy Wilkinson + * + */ +class ContentTypeLinkExtractor implements LinkExtractor { + + private Map linkExtractors = new HashMap<>(); + + ContentTypeLinkExtractor() { + this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor()); + this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, new HalLinkExtractor()); + } + + ContentTypeLinkExtractor(Map linkExtractors) { + this.linkExtractors.putAll(linkExtractors); + } + + @Override + public Map> extractLinks(MockHttpServletResponse response) + throws IOException { + String contentType = response.getContentType(); + LinkExtractor extractorForContentType = getExtractorForContentType(contentType); + if (extractorForContentType != null) { + return extractorForContentType.extractLinks(response); + } + throw new IllegalStateException( + "No LinkExtractor has been provided and one is not available for the " + + "content type " + contentType); + } + + private LinkExtractor getExtractorForContentType(String contentType) { + if (StringUtils.hasText(contentType)) { + MediaType mediaType = MediaType.parseMediaType(contentType); + for (Entry entry : this.linkExtractors.entrySet()) { + if (mediaType.isCompatibleWith(entry.getKey())) { + return entry.getValue(); + } + } + } + return null; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java new file mode 100644 index 000000000..7bc57fffa --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -0,0 +1,80 @@ +/* + * 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.hypermedia; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.MediaType; + +/** + * {@link LinkExtractor} that extracts links in Hypermedia Application Language (HAL) + * format. + * + * @author Andy Wilkinson + */ +class HalLinkExtractor extends AbstractJsonLinkExtractor { + + static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json"); + + @Override + public Map> extractLinks(Map json) { + Map> extractedLinks = new LinkedHashMap<>(); + Object possibleLinks = json.get("_links"); + if (possibleLinks instanceof Map) { + @SuppressWarnings("unchecked") + Map links = (Map) possibleLinks; + for (Entry entry : links.entrySet()) { + String rel = entry.getKey(); + extractedLinks.put(rel, convertToLinks(entry.getValue(), rel)); + } + } + return extractedLinks; + } + + private static List convertToLinks(Object object, String rel) { + List links = new ArrayList<>(); + if (object instanceof Collection) { + @SuppressWarnings("unchecked") + Collection hrefObjects = (Collection) object; + for (Object hrefObject : hrefObjects) { + maybeAddLink(maybeCreateLink(rel, hrefObject), links); + } + } + else { + maybeAddLink(maybeCreateLink(rel, object), links); + } + return links; + } + + private static Link maybeCreateLink(String rel, Object possibleHref) { + if (possibleHref instanceof String) { + return new Link(rel, (String) possibleHref); + } + return null; + } + + private static void maybeAddLink(Link possibleLink, List links) { + if (possibleLink != null) { + links.add(possibleLink); + } + } +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 4ea7df09b..52c0cccbc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -19,10 +19,10 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.RestDocumentationResultHandler; +import org.springframework.restdocs.snippet.Snippet; /** - * Static factory methods for documenting a RESTful API that utilises Hypermedia. + * Static factory methods for documenting a RESTful API that utilizes Hypermedia. * * @author Andy Wilkinson */ @@ -37,29 +37,89 @@ private HypermediaDocumentation() { * * @param rel The rel of the link * @return a {@code LinkDescriptor} ready for further configuration - * @see RestDocumentationResultHandler#withLinks(LinkDescriptor...) - * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) */ public static LinkDescriptor linkWithRel(String rel) { return new LinkDescriptor(rel); } /** - * Creates a {@code LinkSnippetResultHandler} that will produce a documentation - * snippet for a response's links. + * Returns a handler that will produce a snippet documenting the links in the API + * call's response. Links will be extracted from the response automatically based on + * its content type. + * + * @param descriptors The descriptions of the response's links + * @return the handler + */ + public static Snippet links(LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the links in the API + * call's response. The given {@code attributes} will be available during snippet + * generation. Links will be extracted from the response automatically based on its + * content type. * - * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the links snippet + * @param descriptors The descriptions of the response's links + * @return the handler + */ + public static Snippet links(Map attributes, + LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the links in the API + * call's response. Links will be extracted from the response using the given + * {@code linkExtractor}. + * * @param linkExtractor Used to extract the links from the response * @param descriptors The descriptions of the response's links * @return the handler - * @see RestDocumentationResultHandler#withLinks(LinkDescriptor...) - * @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...) */ - public static LinkSnippetResultHandler documentLinks(String identifier, - Map attributes, LinkExtractor linkExtractor, + public static Snippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinkSnippetResultHandler(identifier, attributes, linkExtractor, + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the links in the API + * call's response. The given {@code attributes} will be available during snippet + * generation. Links will be extracted from the response using the given + * {@code linkExtractor}. + * + * @param attributes Attributes made available during rendering of the links snippet + * @param linkExtractor Used to extract the links from the response + * @param descriptors The descriptions of the response's links + * @return the handler + */ + public static Snippet links(LinkExtractor linkExtractor, + Map attributes, LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, attributes, Arrays.asList(descriptors)); } + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia + * Application Language (HAL) format where the links are found in a map named + * {@code _links}. + * + * @return The extract for HAL-style links + */ + public static LinkExtractor halLinks() { + return new HalLinkExtractor(); + } + + /** + * Returns a {@code LinkExtractor} capable of extracting links in Atom format where + * the links are found in an array named {@code links}. + * + * @return The extractor for Atom-style links + */ + public static LinkExtractor atomLinks() { + return new AtomLinkExtractor(); + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java deleted file mode 100644 index 35c0f778f..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractors.java +++ /dev/null @@ -1,187 +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.hypermedia; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Static factory methods providing a selection of {@link LinkExtractor link extractors} - * for use when documentating a hypermedia-based API. - * - * @author Andy Wilkinson - */ -public abstract class LinkExtractors { - - private LinkExtractors() { - - } - - /** - * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia - * Application Language (HAL) format where the links are found in a map named - * {@code _links}. - * - * @return The extract for HAL-style links - */ - public static LinkExtractor halLinks() { - return new HalLinkExtractor(); - } - - /** - * Returns a {@code LinkExtractor} capable of extracting links in Atom format where - * the links are found in an array named {@code links}. - * - * @return The extractor for Atom-style links - */ - public static LinkExtractor atomLinks() { - return new AtomLinkExtractor(); - } - - /** - * Returns the {@code LinkExtractor} for the given {@code contentType} or {@code null} - * if there is no extractor for the content type. - * - * @param contentType The content type, may include parameters - * @return The extractor for the content type, or {@code null} - */ - public static LinkExtractor extractorForContentType(String contentType) { - if (StringUtils.hasText(contentType)) { - MediaType mediaType = MediaType.parseMediaType(contentType); - if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) { - return atomLinks(); - } - if (mediaType.isCompatibleWith(HalLinkExtractor.HAL_MEDIA_TYPE)) { - return halLinks(); - } - } - return null; - } - - private abstract static class JsonContentLinkExtractor implements LinkExtractor { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Override - @SuppressWarnings("unchecked") - public Map> extractLinks(MockHttpServletResponse response) - throws IOException { - Map jsonContent = this.objectMapper.readValue( - response.getContentAsString(), Map.class); - return extractLinks(jsonContent); - } - - protected abstract Map> extractLinks(Map json); - } - - @SuppressWarnings("unchecked") - static class HalLinkExtractor extends JsonContentLinkExtractor { - - private static final MediaType HAL_MEDIA_TYPE = new MediaType("application", - "hal+json"); - - @Override - public Map> extractLinks(Map json) { - Map> extractedLinks = new LinkedHashMap<>(); - Object possibleLinks = json.get("_links"); - if (possibleLinks instanceof Map) { - Map links = (Map) possibleLinks; - for (Entry entry : links.entrySet()) { - String rel = entry.getKey(); - extractedLinks.put(rel, convertToLinks(entry.getValue(), rel)); - } - } - return extractedLinks; - } - - private static List convertToLinks(Object object, String rel) { - List links = new ArrayList<>(); - if (object instanceof Collection) { - Collection hrefObjects = (Collection) object; - for (Object hrefObject : hrefObjects) { - maybeAddLink(maybeCreateLink(rel, hrefObject), links); - } - } - else { - maybeAddLink(maybeCreateLink(rel, object), links); - } - return links; - } - - private static Link maybeCreateLink(String rel, Object possibleHref) { - if (possibleHref instanceof String) { - return new Link(rel, (String) possibleHref); - } - return null; - } - - private static void maybeAddLink(Link possibleLink, List links) { - if (possibleLink != null) { - links.add(possibleLink); - } - } - } - - @SuppressWarnings("unchecked") - static class AtomLinkExtractor extends JsonContentLinkExtractor { - - @Override - public Map> extractLinks(Map json) { - MultiValueMap extractedLinks = new LinkedMultiValueMap<>(); - Object possibleLinks = json.get("links"); - if (possibleLinks instanceof Collection) { - Collection linksCollection = (Collection) possibleLinks; - for (Object linkObject : linksCollection) { - if (linkObject instanceof Map) { - Link link = maybeCreateLink((Map) linkObject); - maybeStoreLink(link, extractedLinks); - } - } - } - return extractedLinks; - } - - private static Link maybeCreateLink(Map linkMap) { - Object hrefObject = linkMap.get("href"); - Object relObject = linkMap.get("rel"); - if (relObject instanceof String && hrefObject instanceof String) { - return new Link((String) relObject, (String) hrefObject); - } - return null; - } - - private static void maybeStoreLink(Link link, - MultiValueMap extractedLinks) { - if (link != null) { - extractedLinks.add(link.getRel(), link); - } - } - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java similarity index 65% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 68155007f..3557581b7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -26,29 +26,33 @@ import java.util.Map.Entry; import java.util.Set; -import org.springframework.restdocs.snippet.SnippetGenerationException; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; /** - * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful - * resource's links. + * A {@link Snippet} that documents a RESTful resource's links. * * @author Andy Wilkinson */ -public class LinkSnippetResultHandler extends SnippetWritingResultHandler { +class LinksSnippet extends TemplatedSnippet { private final Map descriptorsByRel = new LinkedHashMap<>(); private final Set requiredRels = new HashSet<>(); - private final LinkExtractor extractor; + private final LinkExtractor linkExtractor; - LinkSnippetResultHandler(String identifier, Map attributes, - LinkExtractor linkExtractor, List descriptors) { - super(identifier, "links", attributes); - this.extractor = linkExtractor; + LinksSnippet(LinkExtractor linkExtractor, List descriptors) { + this(linkExtractor, null, descriptors); + } + + LinksSnippet(LinkExtractor linkExtractor, Map attributes, + List descriptors) { + super("links", attributes); + this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getRel()); Assert.hasText(descriptor.getDescription()); @@ -60,31 +64,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } @Override - protected Map doHandle(MvcResult result) throws IOException { - validate(extractLinks(result)); + protected Map document(MvcResult result) throws IOException { + validate(this.linkExtractor.extractLinks(result.getResponse())); Map model = new HashMap<>(); model.put("links", createLinksModel()); return model; } - private Map> extractLinks(MvcResult result) throws IOException { - if (this.extractor != null) { - return this.extractor.extractLinks(result.getResponse()); - } - else { - String contentType = result.getResponse().getContentType(); - LinkExtractor extractorForContentType = LinkExtractors - .extractorForContentType(contentType); - if (extractorForContentType != null) { - return extractorForContentType.extractLinks(result.getResponse()); - } - throw new IllegalStateException( - "No LinkExtractor has been provided and one is not available for the content type " - + contentType); - - } - } - private void validate(Map> links) { Set actualRels = links.keySet(); @@ -104,10 +90,10 @@ private void validate(Map> links) { if (message.length() > 0) { message += ". "; } - message += "Links with the following relations were not found in the response: " - + missingRels; + message += "Links with the following relations were not found in the " + + "response: " + missingRels; } - throw new SnippetGenerationException(message); + throw new SnippetException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java similarity index 81% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 0eb0b1f3b..d9dd6df18 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -25,20 +25,21 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.TemplatedSnippet; 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. + * A {@link TemplatedSnippet} 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 { +public abstract class AbstractFieldsSnippet extends + TemplatedSnippet { private final Map descriptorsByPath = new LinkedHashMap(); @@ -50,9 +51,9 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private List fieldDescriptors; - FieldSnippetResultHandler(String identifier, String type, - Map attributes, List descriptors) { - super(identifier, type + "-fields", attributes); + AbstractFieldsSnippet(String type, Map attributes, + List descriptors) { + super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); @@ -62,7 +63,7 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand } @Override - protected Map doHandle(MvcResult result) throws IOException { + protected Map document(MvcResult result) throws IOException { this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); Object payload = extractPayload(result); Map model = new HashMap<>(); @@ -80,8 +81,8 @@ protected Map doHandle(MvcResult result) throws IOException { private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { try { - return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( - descriptor.getPath(), payload); + return AbstractFieldsSnippet.this.fieldTypeResolver + .resolveFieldType(descriptor.getPath(), payload); } catch (FieldDoesNotExistException ex) { String message = "Cannot determine the type of the field '" 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 index 9055cc878..82e43717e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.snippet.SnippetException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -60,7 +60,7 @@ void validate(Reader payloadReader, List fieldDescriptors) message += "Fields with the following paths were not found in the payload: " + missingFields; } - throw new SnippetGenerationException(message); + throw new SnippetException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 4e9d34c64..40e047046 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.RestDocumentationResultHandler; +import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting a RESTful API's request and response payloads. @@ -89,57 +89,92 @@ private PayloadDocumentation() { * * @param path The path of the field * @return a {@code FieldDescriptor} ready for further configuration - * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) - * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) */ public static FieldDescriptor fieldWithPath(String path) { return new FieldDescriptor(path); } /** - * Creates a {@code RequestFieldsSnippetResultHandler} that will produce a - * documentation snippet for a request's fields. + * Returns a handler that will produce a snippet documenting the fields of the API + * call's request. *

- * If a field is present in the request but is not documented by one of the - * descriptors a failure will occur when the handler is invoked. Similarly, if a field - * is documented, is not marked as optional, and is not present in the request a - * failure will also occur. For payloads with a hierarchical structure, documenting a - * field is sufficient for all of its descendants to also be treated as having been + * If a field is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been * documented. * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the request's fields * @return the handler - * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...) * @see #fieldWithPath(String) */ - public static FieldSnippetResultHandler documentRequestFields(String identifier, - Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldSnippetResultHandler(identifier, attributes, + public static Snippet requestFields(FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the fields of the API + * call's request. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a field is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the request's fields + * @return the handler + * @see #fieldWithPath(String) + */ + public static Snippet requestFields(Map attributes, + FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors)); } /** - * Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a - * documentation snippet for a response's fields. + * Returns a handler that will produce a snippet documenting the fields of the API + * call's response. *

- * If a field is present in the response but is not documented by one of the - * descriptors a failure will occur when the handler is invoked. Similarly, if a field - * is documented, is not marked as optional, and is not present in the response a - * failure will also occur. For payloads with a hierarchical structure, documenting a - * field is sufficient for all of its descendants to also be treated as having been + * If a field is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been * documented. * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the response's fields * @return the handler - * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) + * @see #fieldWithPath(String) + */ + public static Snippet responseFields(FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the fields of the API + * call's response. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a field is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the handler is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the response's fields + * @return the handler + * @see #fieldWithPath(String) */ - public static FieldSnippetResultHandler documentResponseFields(String identifier, - Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldSnippetResultHandler(identifier, attributes, + public static Snippet responseFields(Map attributes, + FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(attributes, 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/RequestFieldsSnippet.java similarity index 71% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 851204d2a..bff0832fd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -20,18 +20,22 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; /** - * A {@link FieldSnippetResultHandler} for documenting a request's fields + * A {@link Snippet} that documents the fields in a request. * * @author Andy Wilkinson */ -public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler { +class RequestFieldsSnippet extends AbstractFieldsSnippet { - RequestFieldSnippetResultHandler(String identifier, Map attributes, - List descriptors) { - super(identifier, "request", attributes, descriptors); + RequestFieldsSnippet(List descriptors) { + this(null, descriptors); + } + + RequestFieldsSnippet(Map attributes, List descriptors) { + super("request", attributes, descriptors); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java similarity index 74% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index e5af663c6..2972d80df 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -21,18 +21,23 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; /** - * A {@link FieldSnippetResultHandler} for documenting a response's fields + * A {@link Snippet} the documents the fields in a response. * * @author Andy Wilkinson */ -public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler { +class ResponseFieldsSnippet extends AbstractFieldsSnippet { - ResponseFieldSnippetResultHandler(String identifier, Map attributes, + ResponseFieldsSnippet(List descriptors) { + this(null, descriptors); + } + + ResponseFieldsSnippet(Map attributes, List descriptors) { - super(identifier, "response", attributes, descriptors); + super("response", attributes, descriptors); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java similarity index 81% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index cc4e630bc..35a3ba2a1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -10,19 +10,18 @@ import java.util.Map.Entry; import java.util.Set; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; -public abstract class AbstractParametersSnippetResultHandler extends - SnippetWritingResultHandler { +abstract class AbstractParametersSnippet extends + TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); - protected AbstractParametersSnippetResultHandler(String identifier, - String snippetName, Map attributes, - ParameterDescriptor... descriptors) { - super(identifier, snippetName, attributes); + protected AbstractParametersSnippet(String snippetName, + Map attributes, List descriptors) { + super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getName()); Assert.hasText(descriptor.getDescription()); @@ -31,7 +30,7 @@ protected AbstractParametersSnippetResultHandler(String identifier, } @Override - protected Map doHandle(MvcResult result) throws IOException { + protected Map document(MvcResult result) throws IOException { verifyParameterDescriptors(result); Map model = new HashMap<>(); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java similarity index 79% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 70465a1ab..91bf624d2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -17,30 +17,33 @@ package org.springframework.restdocs.request; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.restdocs.snippet.SnippetGenerationException; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; /** - * A {@link SnippetWritingResultHandler} that produces a snippet documenting the path - * parameters supported by a RESTful resource. + * A {@link Snippet} that documents the path parameters supported by a RESTful resource. * * @author Andy Wilkinson */ -public class PathParametersSnippetResultHandler extends - AbstractParametersSnippetResultHandler { +class PathParametersSnippet extends AbstractParametersSnippet { private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); - protected PathParametersSnippetResultHandler(String identifier, - Map attributes, ParameterDescriptor... descriptors) { - super(identifier, "path-parameters", attributes, descriptors); + PathParametersSnippet(List descriptors) { + this(null, descriptors); + } + + PathParametersSnippet(Map attributes, + List descriptors) { + super("path-parameters", attributes, descriptors); } @Override @@ -84,7 +87,7 @@ protected void verificationFailed(Set undocumentedParameters, message += "Path parameters with the following names were not found in " + "the request: " + missingParameters; } - throw new SnippetGenerationException(message); + throw new SnippetException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java similarity index 69% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java index 45a3f9246..0bd11b88d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java @@ -16,25 +16,28 @@ package org.springframework.restdocs.request; +import java.util.List; import java.util.Map; import java.util.Set; -import org.springframework.restdocs.snippet.SnippetGenerationException; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.test.web.servlet.MvcResult; /** - * A {@link SnippetWritingResultHandler} that produces a snippet documenting the query - * parameters supported by a RESTful resource. + * A {@link Snippet} that documents the query parameters supported by a RESTful resource. * * @author Andy Wilkinson */ -public class QueryParametersSnippetResultHandler extends - AbstractParametersSnippetResultHandler { +class QueryParametersSnippet extends AbstractParametersSnippet { - protected QueryParametersSnippetResultHandler(String identifier, - Map attributes, ParameterDescriptor... descriptors) { - super(identifier, "query-parameters", attributes, descriptors); + QueryParametersSnippet(List descriptors) { + this(null, descriptors); + } + + QueryParametersSnippet(Map attributes, + List descriptors) { + super("query-parameters", attributes, descriptors); } @Override @@ -52,7 +55,7 @@ protected void verificationFailed(Set undocumentedParameters, message += "Query parameters with the following names were not found in the request: " + missingParameters; } - throw new SnippetGenerationException(message); + throw new SnippetException(message); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 3b45c3ecd..fea071e32 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -16,10 +16,10 @@ package org.springframework.restdocs.request; +import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.RestDocumentationResultHandler; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -33,48 +33,68 @@ private RequestDocumentation() { } /** - * Creates a {@link SnippetWritingResultHandler} that will produce a snippet - * documenting a request's path parameters. + * Creates a {@link ParameterDescriptor} that describes a query string parameter with + * the given {@code name}. + * + * @param name The name of the parameter + * @return a {@link ParameterDescriptor} ready for further configuration + */ + public static ParameterDescriptor parameterWithName(String name) { + return new ParameterDescriptor(name); + } + + /** + * Returns a handler that will produce a snippet documenting the path parameters from + * the API call's request. + * + * @param descriptors The descriptions of the parameters in the request's path + * @return the handler + */ + public static Snippet pathParameters(ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the path parameters from + * the API call's request. The given {@code attributes} will be available during + * snippet generation. * - * @param identifier An identifier for the API call that is being documented * @param attributes Attributes made available during rendering of the path parameters * snippet * @param descriptors The descriptions of the parameters in the request's path - * @return the result handler - * @see RestDocumentationResultHandler#withPathParameters(ParameterDescriptor...) + * @return the handler */ - public static SnippetWritingResultHandler documentPathParameters(String identifier, - Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippetResultHandler(identifier, attributes, descriptors); + public static Snippet pathParameters(Map attributes, + ParameterDescriptor... descriptors) { + return new PathParametersSnippet(attributes, + Arrays.asList(descriptors)); } /** - * Creates a {@link SnippetWritingResultHandler} that will produce a snippet - * documenting a request's query parameters + * Returns a handler that will produce a snippet documenting the query parameters from + * the API call's request. * - * @param identifier An identifier for the API call that is being documented - * @param attributes Attributes made available during rendering of the query - * parameters snippet - * @param descriptors The descriptions of the parameters in the request's query string - * @return the result handler - * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) + * @param descriptors The descriptions of the request's query parameters + * @return the handler */ - public static SnippetWritingResultHandler documentQueryParameters(String identifier, - Map attributes, ParameterDescriptor... descriptors) { - return new QueryParametersSnippetResultHandler(identifier, attributes, - descriptors); + public static Snippet queryParameters(ParameterDescriptor... descriptors) { + return new QueryParametersSnippet(Arrays.asList(descriptors)); } /** - * Creates a {@link ParameterDescriptor} that describes a query string parameter with - * the given {@code name}. + * Returns a handler that will produce a snippet documenting the query parameters from + * the API call's request. The given {@code attributes} will be available during + * snippet generation. * - * @param name The name of the parameter - * @return a {@link ParameterDescriptor} ready for further configuration - * @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...) + * @param attributes Attributes made available during rendering of the query + * parameters snippet + * @param descriptors The descriptions of the request's query parameters + * @return the handler */ - public static ParameterDescriptor parameterWithName(String name) { - return new ParameterDescriptor(name); + public static Snippet queryParameters(Map attributes, + ParameterDescriptor... descriptors) { + return new QueryParametersSnippet(attributes, + Arrays.asList(descriptors)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java index 977eaf5c6..44d78999c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -67,7 +67,7 @@ private static class ContentModifyingMethodInterceptor implements MethodIntercep private final MockHttpServletResponse delegate; - public ContentModifyingMethodInterceptor(String modifiedContent, + private ContentModifyingMethodInterceptor(String modifiedContent, MockHttpServletResponse delegate) { this.modifiedContent = modifiedContent; this.delegate = delegate; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java index c7cc69a10..53da363e5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java @@ -42,7 +42,7 @@ class HeaderRemovingResponsePostProcessor implements ResponsePostProcessor { private final Set headersToRemove; - public HeaderRemovingResponsePostProcessor(String... headersToRemove) { + HeaderRemovingResponsePostProcessor(String... headersToRemove) { this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java index 9f552e1e5..db8462f39 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java @@ -75,12 +75,11 @@ private AttributeBuilder(String key) { public Attribute value(Object value) { return new Attribute(this.key, value); } + } /** * An attribute (key-value pair). - * - * @author Andy Wilkinson */ public static class Attribute { @@ -88,18 +87,32 @@ public static class Attribute { private final Object value; + /** + * Creates a new attribute with the given {@code key} and {@code value}. + * @param key the key + * @param value the value + */ public Attribute(String key, Object value) { this.key = key; this.value = value; } + /** + * Returns the attribute's key + * @return the key + */ public String getKey() { return this.key; } + /** + * Returns the attribute's value + * @return the value + */ public Object getValue() { return this.value; } } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java new file mode 100644 index 000000000..323af9e2b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.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.snippet; + +import java.io.IOException; + +import org.springframework.test.web.servlet.MvcResult; + +/** + * A {@link Snippet} is used to document aspects of a call to a RESTful API. + * + * @author Andy Wilkinson + */ +public interface Snippet { + + /** + * Documents the call to the RESTful API described by the given {@code result}. The + * call is identified by the given {@code operation}. + * + * @param operation the API operation + * @param result the result of the operation + * @throws IOException if a failure occurs will documenting the result + */ + void document(String operation, MvcResult result) throws IOException; + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetException.java similarity index 81% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetException.java index 027d82825..f846b8f32 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetGenerationException.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetException.java @@ -23,14 +23,13 @@ * @author Andy Wilkinson */ @SuppressWarnings("serial") -public class SnippetGenerationException extends RuntimeException { +public class SnippetException extends RuntimeException { /** - * Creates a new {@code SnippetGenerationException} described by the given - * {@code message} + * Creates a new {@code SnippetException} described by the given {@code message} * @param message the message that describes the problem */ - public SnippetGenerationException(String message) { + public SnippetException(String message) { super(message); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java similarity index 70% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 12b364eb1..de9f966a2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -21,26 +21,23 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.restdocs.templates.Template; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultHandler; /** - * Base class for a {@link ResultHandler} that writes a documentation snippet + * Base class for a {@link Snippet} that is produced using a {@link Template} and + * {@link TemplateEngine}. * * @author Andy Wilkinson */ -public abstract class SnippetWritingResultHandler implements ResultHandler { +public abstract class TemplatedSnippet implements Snippet { private final Map attributes = new HashMap<>(); - private final String identifier; - private final String snippetName; - protected SnippetWritingResultHandler(String identifier, String snippetName, - Map attributes) { - this.identifier = identifier; + protected TemplatedSnippet(String snippetName, Map attributes) { this.snippetName = snippetName; if (attributes != null) { this.attributes.putAll(attributes); @@ -48,11 +45,11 @@ protected SnippetWritingResultHandler(String identifier, String snippetName, } @Override - public void handle(MvcResult result) throws IOException { + public void document(String operation, MvcResult result) throws IOException { WriterResolver writerResolver = (WriterResolver) result.getRequest() .getAttribute(WriterResolver.class.getName()); - try (Writer writer = writerResolver.resolve(this.identifier, this.snippetName)) { - Map model = doHandle(result); + try (Writer writer = writerResolver.resolve(operation, this.snippetName)) { + Map model = document(result); model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); @@ -60,6 +57,6 @@ public void handle(MvcResult result) throws IOException { } } - protected abstract Map doHandle(MvcResult result) throws IOException; + protected abstract Map document(MvcResult result) throws IOException; } \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index bc84de534..f3642d76a 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -23,9 +23,15 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; @@ -112,14 +118,14 @@ public void basicSnippetGeneration() throws Exception { } @Test - public void links() throws Exception { + public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links").withLinks( - linkWithRel("rel").description("The description"))); + .andDo(document("links", + links(linkWithRel("rel").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -127,14 +133,14 @@ public void links() throws Exception { } @Test - public void pathParameters() throws Exception { + public void pathParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links").withPathParameters( - parameterWithName("foo").description("The description"))); + .andDo(document("links", pathParameters(parameterWithName("foo") + .description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -142,14 +148,14 @@ public void pathParameters() throws Exception { } @Test - public void queryParameters() throws Exception { + public void queryParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links").withQueryParameters( - parameterWithName("foo").description("The description"))); + .andDo(document("links", queryParameters(parameterWithName("foo") + .description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -157,7 +163,7 @@ public void queryParameters() throws Exception { } @Test - public void requestFields() throws Exception { + public void requestFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); @@ -165,8 +171,8 @@ public void requestFields() throws Exception { get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links").withRequestFields( - fieldWithPath("a").description("The description"))); + .andDo(document("links", + requestFields(fieldWithPath("a").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -174,15 +180,18 @@ public void requestFields() throws Exception { } @Test - public void responseFields() throws Exception { + public void responseFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links").withResponseFields( - fieldWithPath("a").description("The description"), - fieldWithPath("links").description("Links to other resources"))); + .andDo(document( + "links", + responseFields( + fieldWithPath("a").description("The description"), + fieldWithPath("links").description( + "Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -284,8 +293,10 @@ public void customSnippetTemplate() throws Exception { is(snippet().withContents(equalTo("Custom curl request")))); mockMvc.perform(get("/")).andDo( - document("index").withCurlRequest( - attributes(key("title").value("Access the index using curl")))); + document( + "index", + curlRequest(attributes(key("title").value( + "Access the index using curl"))))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 1f684b176..b8344b2f4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -43,7 +43,7 @@ public class RestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); - private RestDocumentationContext context = new RestDocumentationContext(); + private RestDocumentationContext context = new RestDocumentationContext(null); @Before public void establishContext() { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java new file mode 100644 index 000000000..629d6908b --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java @@ -0,0 +1,6 @@ +package org.springframework.restdocs.config; + + +public class RestDocumentationContexts { + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java similarity index 84% rename from spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java index c0b8f808d..300cf6fa8 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java @@ -23,7 +23,6 @@ import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; @@ -44,14 +43,14 @@ import org.springframework.restdocs.test.ExpectedSnippet; /** - * Tests for {@link CurlDocumentation} + * Tests for {@link CurlRequestSnippet} * * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda * @author Jonathan Pearlin */ -public class CurlDocumentationTests { +public class CurlRequestDocumentationHandlerTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(); @@ -63,14 +62,16 @@ public class CurlDocumentationTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - documentCurlRequest("get-request", null).handle(result(get("/foo"))); + new CurlRequestSnippet() + .document("get-request", result(get("/foo"))); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - documentCurlRequest("non-get-request", null).handle(result(post("/foo"))); + new CurlRequestSnippet().document("non-get-request", + result(post("/foo"))); } @Test @@ -78,7 +79,7 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - documentCurlRequest("request-with-content", null).handle( + new CurlRequestSnippet().document("request-with-content", result(get("/foo").content("content"))); } @@ -88,7 +89,7 @@ public void requestWithQueryString() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); - documentCurlRequest("request-with-query-string", null).handle( + new CurlRequestSnippet().document("request-with-query-string", result(get("/foo?param=value"))); } @@ -96,7 +97,7 @@ public void requestWithQueryString() throws IOException { public void requestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("request-with-one-parameter").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); - documentCurlRequest("request-with-one-parameter", null).handle( + new CurlRequestSnippet().document("request-with-one-parameter", result(get("/foo").param("k1", "v1"))); } @@ -105,9 +106,9 @@ public void requestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); - documentCurlRequest("request-with-multiple-parameters", null).handle( - result(get("/foo").param("k1", "v1").param("k2", "v2") - .param("k1", "v1-bis"))); + new CurlRequestSnippet().document( + "request-with-multiple-parameters", result(get("/foo").param("k1", "v1") + .param("k2", "v2").param("k1", "v1-bis"))); } @Test @@ -116,7 +117,8 @@ public void requestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); - documentCurlRequest("request-with-url-encoded-parameter", null).handle( + new CurlRequestSnippet().document( + "request-with-url-encoded-parameter", result(get("/foo").param("k1", "foo bar&"))); } @@ -125,7 +127,7 @@ public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); - documentCurlRequest("post-request-with-one-parameter", null).handle( + new CurlRequestSnippet().document("post-request-with-one-parameter", result(post("/foo").param("k1", "v1"))); } @@ -136,7 +138,8 @@ public void postRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - documentCurlRequest("post-request-with-multiple-parameters", null).handle( + new CurlRequestSnippet().document( + "post-request-with-multiple-parameters", result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @@ -147,7 +150,8 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - documentCurlRequest("post-request-with-url-encoded-parameter", null).handle( + new CurlRequestSnippet().document( + "post-request-with-url-encoded-parameter", result(post("/foo").param("k1", "a&b"))); } @@ -156,7 +160,7 @@ public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - documentCurlRequest("put-request-with-one-parameter", null).handle( + new CurlRequestSnippet().document("put-request-with-one-parameter", result(put("/foo").param("k1", "v1"))); } @@ -167,7 +171,8 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - documentCurlRequest("put-request-with-multiple-parameters", null).handle( + new CurlRequestSnippet().document( + "put-request-with-multiple-parameters", result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); } @@ -177,7 +182,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - documentCurlRequest("put-request-with-url-encoded-parameter", null).handle( + new CurlRequestSnippet().document( + "put-request-with-url-encoded-parameter", result(put("/foo").param("k1", "a&b"))); } @@ -187,7 +193,8 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - documentCurlRequest("request-with-headers", null).handle( + new CurlRequestSnippet().document( + "request-with-headers", result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", "alpha"))); } @@ -198,7 +205,8 @@ public void httpWithNonStandardPort() throws IOException { codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8080); - documentCurlRequest("http-with-non-standard-port", null).handle(result(request)); + new CurlRequestSnippet().document("http-with-non-standard-port", + result(request)); } @Test @@ -208,7 +216,8 @@ public void httpsWithStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(443); request.setScheme("https"); - documentCurlRequest("https-with-standard-port", null).handle(result(request)); + new CurlRequestSnippet().document("https-with-standard-port", + result(request)); } @Test @@ -218,7 +227,8 @@ public void httpsWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerPort(8443); request.setScheme("https"); - documentCurlRequest("https-with-non-standard-port", null).handle(result(request)); + new CurlRequestSnippet().document("https-with-non-standard-port", + result(request)); } @Test @@ -227,7 +237,8 @@ public void requestWithCustomHost() throws IOException { codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentCurlRequest("request-with-custom-host", null).handle(result(request)); + new CurlRequestSnippet().document("request-with-custom-host", + result(request)); } @Test @@ -239,8 +250,8 @@ public void requestWithContextPathWithSlash() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("/v3"); - documentCurlRequest("request-with-custom-context-with-slash", null).handle( - result(request)); + new CurlRequestSnippet().document( + "request-with-custom-context-with-slash", result(request)); } @Test @@ -252,8 +263,8 @@ public void requestWithContextPathWithoutSlash() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); request.setContextPath("v3"); - documentCurlRequest("request-with-custom-context-without-slash", null).handle( - result(request)); + new CurlRequestSnippet().document( + "request-with-custom-context-without-slash", result(request)); } @Test @@ -265,7 +276,8 @@ public void multipartPostWithNoOriginalFilename() throws IOException { .withContents(codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("metadata", "{\"description\": \"foo\"}".getBytes()); - documentCurlRequest("multipart-post-no-original-filename", null).handle( + new CurlRequestSnippet().document( + "multipart-post-no-original-filename", result(fileUpload("/upload").file(multipartFile))); } @@ -279,7 +291,8 @@ public void multipartPostWithContentType() throws IOException { MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "bytes".getBytes()); - documentCurlRequest("multipart-post-with-content-type", null).handle( + new CurlRequestSnippet().document( + "multipart-post-with-content-type", result(fileUpload("/upload").file(multipartFile))); } @@ -292,7 +305,7 @@ public void multipartPost() throws IOException { codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "bytes".getBytes()); - documentCurlRequest("multipart-post", null).handle( + new CurlRequestSnippet().document("multipart-post", result(fileUpload("/upload").file(multipartFile))); } @@ -306,7 +319,8 @@ public void multipartPostWithParameters() throws IOException { codeBlock("bash").content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "bytes".getBytes()); - documentCurlRequest("multipart-post", null).handle( + new CurlRequestSnippet().document( + "multipart-post", result(fileUpload("/upload").file(multipartFile) .param("a", "apple", "avocado").param("b", "banana"))); } @@ -323,9 +337,8 @@ public void customAttributes() throws IOException { "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - documentCurlRequest("custom-attributes", - attributes(key("title").value("curl request title"))).handle( - result(request)); + new CurlRequestSnippet(attributes(key("title").value( + "curl request title"))).document("custom-attributes", result(request)); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java similarity index 69% rename from spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java index 59b1f5476..beb799979 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java @@ -19,18 +19,13 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.OK; import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; -import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; -import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; @@ -44,7 +39,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -52,12 +46,13 @@ import org.springframework.restdocs.test.ExpectedSnippet; /** - * Tests for {@link HttpDocumentation} - * + * Tests for {@link HttpRequestSnippet} + * * @author Andy Wilkinson * @author Jonathan Pearlin + * */ -public class HttpDocumentationTests { +public class HttpRequestDocumentationHandlerTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; @@ -70,8 +65,8 @@ public void getRequest() throws IOException { httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( "Alpha", "a")); - documentHttpRequest("get-request", null).handle( - result(get("/foo").header("Alpha", "a"))); + new HttpRequestSnippet().document("get-request", result(get("/foo") + .header("Alpha", "a"))); } @Test @@ -79,7 +74,7 @@ public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string").withContents( httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); - documentHttpRequest("get-request-with-query-string", null).handle( + new HttpRequestSnippet().document("get-request-with-query-string", result(get("/foo?bar=baz"))); } @@ -88,7 +83,7 @@ public void getRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("get-request-with-parameter").withContents( httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost")); - documentHttpRequest("get-request-with-parameter", null).handle( + new HttpRequestSnippet().document("get-request-with-parameter", result(get("/foo").param("b&r", "baz"))); } @@ -98,7 +93,7 @@ public void postRequestWithContent() throws IOException { httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - documentHttpRequest("post-request-with-content", null).handle( + new HttpRequestSnippet().document("post-request-with-content", result(post("/foo").content("Hello, world"))); } @@ -109,7 +104,7 @@ public void postRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - documentHttpRequest("post-request-with-parameter", null).handle( + new HttpRequestSnippet().document("post-request-with-parameter", result(post("/foo").param("b&r", "baz").param("a", "alpha"))); } @@ -119,7 +114,7 @@ public void putRequestWithContent() throws IOException { httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - documentHttpRequest("put-request-with-content", null).handle( + new HttpRequestSnippet().document("put-request-with-content", result(put("/foo").content("Hello, world"))); } @@ -130,48 +125,10 @@ public void putRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - documentHttpRequest("put-request-with-parameter", null).handle( + new HttpRequestSnippet().document("put-request-with-parameter", result(put("/foo").param("b&r", "baz").param("a", "alpha"))); } - @Test - public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); - documentHttpResponse("basic-response", null).handle(result()); - } - - @Test - public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse("non-ok-response").withContents( - httpResponse(BAD_REQUEST)); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(BAD_REQUEST.value()); - documentHttpResponse("non-ok-response", null).handle(result(response)); - } - - @Test - public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse("response-with-headers").withContents( - httpResponse(OK) // - .header("Content-Type", "application/json") // - .header("a", "alpha")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - documentHttpResponse("response-with-headers", null).handle(result(response)); - } - - @Test - public void responseWithContent() throws IOException { - this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(OK).content("content")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("content"); - documentHttpResponse("response-with-content", null).handle(result(response)); - } - @Test public void multipartPost() throws IOException { String expectedContent = createPart(String.format("Content-Disposition: " @@ -184,7 +141,7 @@ public void multipartPost() throws IOException { .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); - documentHttpRequest("multipart-post", null).handle( + new HttpRequestSnippet().document("multipart-post", result(fileUpload("/upload").file(multipartFile))); } @@ -207,7 +164,8 @@ public void multipartPostWithParameters() throws IOException { .content(expectedContent)); MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", null, "<< data >>".getBytes()); - documentHttpRequest("multipart-post", null).handle( + new HttpRequestSnippet().document( + "multipart-post", result(fileUpload("/upload").file(multipartFile) .param("a", "apple", "avocado").param("b", "banana"))); } @@ -226,7 +184,8 @@ public void multipartPostWithContentType() throws IOException { MockMultipartFile multipartFile = new MockMultipartFile("image", "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "<< data >>".getBytes()); - documentHttpRequest("multipart-post-with-content-type", null).handle( + new HttpRequestSnippet().document( + "multipart-post-with-content-type", result(fileUpload("/upload").file(multipartFile))); } @@ -238,7 +197,7 @@ public void getRequestWithCustomServerName() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setServerName("api.example.com"); - documentHttpRequest("get-request-custom-server-name", null).handle( + new HttpRequestSnippet().document("get-request-custom-server-name", result(request)); } @@ -247,7 +206,7 @@ public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - documentHttpRequest("get-request-custom-host", null).handle( + new HttpRequestSnippet().document("get-request-custom-host", result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); } @@ -263,25 +222,8 @@ public void requestWithCustomSnippetAttributes() throws IOException { "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - documentHttpRequest("request-with-snippet-attributes", - attributes(key("title").value("Title for the request"))).handle( - result(request)); - } - - @Test - public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( - containsString("Title for the response")); - MockHttpServletRequest request = new MockHttpServletRequest(); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("http-response")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - documentHttpResponse("response-with-snippet-attributes", - attributes(key("title").value("Title for the response"))).handle( + new HttpRequestSnippet(attributes(key("title").value( + "Title for the request"))).document("request-with-snippet-attributes", result(request)); } @@ -290,12 +232,12 @@ private String createPart(String content) { } private String createPart(String content, boolean last) { - String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; StringBuilder part = new StringBuilder(); - part.append(String.format("--%s%n%s%n", boundary, content)); + part.append(String.format("--%s%n%s%n", BOUNDARY, content)); if (last) { - part.append(String.format("--%s--", boundary)); + part.append(String.format("--%s--", BOUNDARY)); } return part.toString(); } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java new file mode 100644 index 000000000..c9983761c --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java @@ -0,0 +1,111 @@ +/* + * 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.http; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import java.io.IOException; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link HttpResponseSnippet} + * + * @author Andy Wilkinson + * @author Jonathan Pearlin + */ +public class HttpResponseDocumentationHandlerTests { + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void basicResponse() throws IOException { + this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); + new HttpResponseSnippet().document("basic-response", result()); + } + + @Test + public void nonOkResponse() throws IOException { + this.snippet.expectHttpResponse("non-ok-response").withContents( + httpResponse(BAD_REQUEST)); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(BAD_REQUEST.value()); + new HttpResponseSnippet().document("non-ok-response", + result(response)); + } + + @Test + public void responseWithHeaders() throws IOException { + this.snippet.expectHttpResponse("response-with-headers").withContents( + httpResponse(OK) // + .header("Content-Type", "application/json") // + .header("a", "alpha")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setHeader("a", "alpha"); + new HttpResponseSnippet().document("response-with-headers", + result(response)); + } + + @Test + public void responseWithContent() throws IOException { + this.snippet.expectHttpResponse("response-with-content").withContents( + httpResponse(OK).content("content")); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("content"); + new HttpResponseSnippet().document("response-with-content", + result(response)); + } + + @Test + public void responseWithCustomSnippetAttributes() throws IOException { + this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( + containsString("Title for the response")); + MockHttpServletRequest request = new MockHttpServletRequest(); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("http-response")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + new HttpResponseSnippet(attributes(key("title").value( + "Title for the response"))).document("response-with-snippet-attributes", + result(request)); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java new file mode 100644 index 000000000..1c258a262 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -0,0 +1,70 @@ +/* + * 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.hypermedia; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Tests for {@link ContentTypeLinkExtractor}. + * + * @author Andy Wilkinson + */ +public class ContentTypeLinkExtractorTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final MockHttpServletResponse response = new MockHttpServletResponse(); + + @Test + public void extractionFailsWithNullContentType() throws IOException { + this.thrown.expect(IllegalStateException.class); + new ContentTypeLinkExtractor().extractLinks(this.response); + } + + @Test + public void extractorCalledWithMatchingContextType() throws IOException { + Map extractors = new HashMap<>(); + LinkExtractor extractor = mock(LinkExtractor.class); + extractors.put(MediaType.APPLICATION_JSON, extractor); + this.response.setContentType("application/json"); + new ContentTypeLinkExtractor(extractors).extractLinks(this.response); + verify(extractor).extractLinks(this.response); + } + + @Test + public void extractorCalledWithCompatibleContextType() throws IOException { + Map extractors = new HashMap<>(); + LinkExtractor extractor = mock(LinkExtractor.class); + extractors.put(MediaType.APPLICATION_JSON, extractor); + this.response.setContentType("application/json;foo=bar"); + new ContentTypeLinkExtractor(extractors).extractLinks(this.response); + verify(extractor).extractLinks(this.response); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 036aa1b5d..9e6256d7f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -37,7 +37,8 @@ import org.springframework.util.MultiValueMap; /** - * Parameterized tests for {@link LinkExtractors} with various payloads. + * Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with + * various payloads. * * @author Andy Wilkinson */ @@ -50,8 +51,8 @@ public class LinkExtractorsPayloadTests { @Parameters public static Collection data() { - return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, - new Object[] { LinkExtractors.atomLinks(), "atom" }); + return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" }, + new Object[] { new AtomLinkExtractor(), "atom" }); } public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java deleted file mode 100644 index 89cba6f06..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsTests.java +++ /dev/null @@ -1,65 +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.hypermedia; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; - -import org.junit.Test; -import org.springframework.restdocs.hypermedia.LinkExtractors.AtomLinkExtractor; -import org.springframework.restdocs.hypermedia.LinkExtractors.HalLinkExtractor; - -/** - * Tests for {@link LinkExtractors}. - * - * @author Andy Wilkinson - */ -public class LinkExtractorsTests { - - @Test - public void nullContentTypeYieldsNullExtractor() { - assertThat(LinkExtractors.extractorForContentType(null), nullValue()); - } - - @Test - public void emptyContentTypeYieldsNullExtractor() { - assertThat(LinkExtractors.extractorForContentType(""), nullValue()); - } - - @Test - public void applicationJsonContentTypeYieldsAtomExtractor() { - LinkExtractor linkExtractor = LinkExtractors - .extractorForContentType("application/json"); - assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class)); - } - - @Test - public void applicationHalJsonContentTypeYieldsHalExtractor() { - LinkExtractor linkExtractor = LinkExtractors - .extractorForContentType("application/hal+json"); - assertThat(linkExtractor, instanceOf(HalLinkExtractor.class)); - } - - @Test - public void contentTypeWithParameterYieldsExtractor() { - LinkExtractor linkExtractor = LinkExtractors - .extractorForContentType("application/json;foo=bar"); - assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class)); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java similarity index 69% rename from spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java index 6fb0d58da..71943b109 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java @@ -20,13 +20,14 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; import org.junit.Rule; import org.junit.Test; @@ -34,7 +35,7 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -43,11 +44,11 @@ import org.springframework.util.MultiValueMap; /** - * Tests for {@link HypermediaDocumentation} + * Tests for {@link LinksSnippet} * * @author Andy Wilkinson */ -public class HypermediaDocumentationTests { +public class LinksDocumentationHandlerTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(); @@ -57,21 +58,22 @@ public class HypermediaDocumentationTests { @Test public void undocumentedLink() throws IOException { - this.thrown.expect(SnippetGenerationException.class); + this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [foo]")); - documentLinks("undocumented-link", null, - new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle( - result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", + "bar")), Collections. emptyList()).document( + "undocumented-link", result()); } @Test public void missingLink() throws IOException { - this.thrown.expect(SnippetGenerationException.class); + this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - documentLinks("missing-link", null, new StubLinkExtractor(), - new LinkDescriptor("foo").description("bar")).handle(result()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( + "missing-link", result()); } @Test @@ -79,9 +81,9 @@ public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - documentLinks("documented-optional-link", null, - new StubLinkExtractor().withLinks(new Link("foo", "blah")), - new LinkDescriptor("foo").description("bar").optional()).handle(result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", + "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar") + .optional())).document("documented-optional-link", result()); } @Test @@ -89,19 +91,20 @@ public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - documentLinks("missing-optional-link", null, new StubLinkExtractor(), - new LinkDescriptor("foo").description("bar").optional()).handle(result()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) + .document("missing-optional-link", result()); } @Test public void undocumentedLinkAndMissingLink() throws IOException { - this.thrown.expect(SnippetGenerationException.class); + this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); - documentLinks("undocumented-link-and-missing-link", null, - new StubLinkExtractor().withLinks(new Link("a", "alpha")), - new LinkDescriptor("foo").description("bar")).handle(result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", + "alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document("undocumented-link-and-missing-link", result()); } @Test @@ -110,12 +113,11 @@ public void documentedLinks() throws IOException { tableWithHeader("Relation", "Description") // .row("a", "one") // .row("b", "two")); - documentLinks( - "documented-links", - null, - new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", - "bravo")), new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")).handle(result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", + "alpha"), new Link("b", "bravo")), Arrays.asList( + new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two"))).document("documented-links", + result()); } @Test @@ -132,20 +134,18 @@ public void linksWithCustomDescriptorAttributes() throws IOException { "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - documentLinks( - "links-with-custom-descriptor-attributes", - null, - new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", - "bravo")), + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", + "alpha"), new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one").attributes( key("foo").value("alpha")), new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo"))).handle(result(request)); + key("foo").value("bravo")))).document( + "links-with-custom-descriptor-attributes", result(request)); } @Test - public void linksWithCustomAttribute() throws IOException { - this.snippet.expectLinks("links-with-custom-attribute").withContents( + public void linksWithCustomAttributes() throws IOException { + this.snippet.expectLinks("links-with-custom-attributes").withContents( startsWith(".Title for the links")); MockHttpServletRequest request = new MockHttpServletRequest(); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); @@ -155,12 +155,12 @@ public void linksWithCustomAttribute() throws IOException { "src/test/resources/custom-snippet-templates/links-with-title.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - documentLinks( - "links-with-custom-attribute", - attributes(key("title").value("Title for the links")), - new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", - "bravo")), new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")).handle(result(request)); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", + "alpha"), new Link("b", "bravo")), attributes(key("title").value( + "Title for the links")), Arrays.asList( + new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two"))).document( + "links-with-custom-attributes", result(request)); } private static class StubLinkExtractor implements LinkExtractor { 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 index e8ec0f299..53f118f4c 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -24,7 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.snippet.SnippetException; /** * Tests for {@link FieldValidator} @@ -73,7 +73,7 @@ public void childIsDocumentedWhenParentIsDocumented() throws IOException { @Test public void missingField() throws IOException { - this.thrownException.expect(SnippetGenerationException.class); + this.thrownException.expect(SnippetException.class); this.thrownException .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [y, z]")); @@ -84,7 +84,7 @@ public void missingField() throws IOException { @Test public void undocumentedField() throws IOException { - this.thrownException.expect(SnippetGenerationException.class); + this.thrownException.expect(SnippetException.class); this.thrownException.expectMessage(equalTo(String .format("The following parts of the payload were not" + " documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}"))); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java deleted file mode 100644 index f23a3ba65..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ /dev/null @@ -1,282 +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.payload; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; - -import java.io.IOException; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.snippet.SnippetGenerationException; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; - -/** - * Tests for {@link PayloadDocumentation} - * - * @author Andy Wilkinson - */ -public class PayloadDocumentationTests { - - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); - - @Test - public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("a.b", "Number", "one") // - .row("a.c", "String", "two") // - .row("a", "Object", "three")); - - documentRequestFields("map-request-with-fields", null, - fieldWithPath("a.b").description("one"), - fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three")).handle( - result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"))); - } - - @Test - public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]a.b", "Number", "one") // - .row("[]a.c", "String", "two") // - .row("[]a", "Object", "three")); - - documentRequestFields("array-request-with-fields", null, - fieldWithPath("[]a.b").description("one"), - fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three")).handle( - result(get("/foo").content( - "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"))); - } - - @Test - public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields("map-response-with-fields").withContents(// - tableWithHeader("Path", "Type", "Description") // - .row("id", "Number", "one") // - .row("date", "String", "two") // - .row("assets", "Array", "three") // - .row("assets[]", "Object", "four") // - .row("assets[].id", "Number", "five") // - .row("assets[].name", "String", "six")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}"); - documentResponseFields("map-response-with-fields", null, - fieldWithPath("id").description("one"), - fieldWithPath("date").description("two"), - fieldWithPath("assets").description("three"), - fieldWithPath("assets[]").description("four"), - fieldWithPath("assets[].id").description("five"), - fieldWithPath("assets[].name").description("six")).handle( - result(response)); - } - - @Test - public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]a.b", "Number", "one") // - .row("[]a.c", "String", "two") // - .row("[]a", "Object", "three")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter() - .append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"); - documentResponseFields("array-response-with-fields", null, - fieldWithPath("[]a.b").description("one"), - fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three")).handle(result(response)); - } - - @Test - public void arrayResponse() throws IOException { - this.snippet.expectResponseFields("array-response").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]", "String", "one")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("[\"a\", \"b\", \"c\"]"); - documentResponseFields("array-response", null, - fieldWithPath("[]").description("one")).handle(result(response)); - } - - @Test - public void undocumentedRequestField() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - documentRequestFields("undocumented-request-fields", null).handle( - result(get("/foo").content("{\"a\": 5}"))); - } - - @Test - public void missingRequestField() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a.b]")); - documentRequestFields("missing-request-fields", null, - fieldWithPath("a.b").description("one")).handle( - result(get("/foo").content("{}"))); - } - - @Test - public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - documentRequestFields("missing-optional-request-field-with-no-type", null, - fieldWithPath("a.b").description("one").optional()).handle( - result(get("/foo").content("{ }"))); - } - - @Test - public void undocumentedRequestFieldAndMissingRequestField() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a.b]")); - documentRequestFields("undocumented-request-field-and-missing-request-field", - null, fieldWithPath("a.b").description("one")).handle( - result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); - } - - @Test - public void requestFieldsWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestFields( - "request-fields-with-custom-descriptor-attributes").withContents( // - tableWithHeader("Path", "Type", "Description", "Foo") // - .row("a.b", "Number", "one", "alpha") // - .row("a.c", "String", "two", "bravo") // - .row("a", "Object", "three", "charlie")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-fields")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); - documentRequestFields( - "request-fields-with-custom-descriptor-attributes", - null, - fieldWithPath("a.b").description("one").attributes( - key("foo").value("alpha")), - fieldWithPath("a.c").description("two").attributes( - key("foo").value("bravo")), - fieldWithPath("a").description("three").attributes( - key("foo").value("charlie"))).handle(result(request)); - } - - @Test - public void responseFieldsWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents( // - tableWithHeader("Path", "Type", "Description", "Foo") // - .row("a.b", "Number", "one", "alpha") // - .row("a.c", "String", "two", "bravo") // - .row("a", "Object", "three", "charlie")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("response-fields")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"); - documentResponseFields( - "response-fields-with-custom-attributes", - null, - fieldWithPath("a.b").description("one").attributes( - key("foo").value("alpha")), - fieldWithPath("a.c").description("two").attributes( - key("foo").value("bravo")), - fieldWithPath("a").description("three").attributes( - key("foo").value("charlie"))).handle(result(request, response)); - } - - @Test - public void requestFieldsWithCustomAttributes() throws IOException { - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-fields")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/request-fields-with-title.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.setContent("{\"a\": \"foo\"}".getBytes()); - MockHttpServletResponse response = new MockHttpServletResponse(); - documentRequestFields("request-fields-with-custom-attributes", - attributes(key("title").value("Custom title")), - fieldWithPath("a").description("one")).handle(result(request, response)); - } - - @Test - public void responseFieldsWithCustomAttributes() throws IOException { - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("response-fields")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/response-fields-with-title.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getOutputStream().print("{\"a\": \"foo\"}"); - documentResponseFields("response-fields-with-custom-attributes", - attributes(key("title").value("Custom title")), - fieldWithPath("a").description("one")).handle(result(request, response)); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java new file mode 100644 index 000000000..4e7722601 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java @@ -0,0 +1,185 @@ +/* + * 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.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link RequestFieldsSnippet} + * + * @author Andy Wilkinson + */ +public class RequestFieldsDocumentationHandlerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void mapRequestWithFields() throws IOException { + this.snippet.expectRequestFields("map-request-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("a.b", "Number", "one") // + .row("a.c", "String", "two") // + .row("a", "Object", "three")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b") + .description("one"), fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three"))).document( + "map-request-with-fields", + result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"))); + } + + @Test + public void arrayRequestWithFields() throws IOException { + this.snippet.expectRequestFields("array-request-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]a.b", "Number", "one") // + .row("[]a.c", "String", "two") // + .row("[]a", "Object", "three")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b") + .description("one"), fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))).document( + "array-request-with-fields", + result(get("/foo").content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"))); + } + + @Test + public void undocumentedRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new RequestFieldsSnippet(Collections. emptyList()) + .document("undocumented-request-field", + result(get("/foo").content("{\"a\": 5}"))); + } + + @Test + public void missingRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a.b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b") + .description("one"))).document("missing-request-fields", + result(get("/foo").content("{}"))); + } + + @Test + public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b") + .description("one").optional())).document( + "missing-optional-request-field-with-no-type", result(get("/foo") + .content("{ }"))); + } + + @Test + public void undocumentedRequestFieldAndMissingRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a.b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b") + .description("one"))).document( + "undocumented-request-field-and-missing-request-field", + result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); + } + + @Test + public void requestFieldsWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestFields( + "request-fields-with-custom-descriptor-attributes").withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("request-fields")).thenReturn( + snippetResource("request-fields-with-extra-column")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("a.b").description("one").attributes( + key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes( + key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes( + key("foo").value("charlie")))).document( + "request-fields-with-custom-descriptor-attributes", result(request)); + } + + @Test + public void requestFieldsWithCustomAttributes() throws IOException { + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("request-fields")).thenReturn( + snippetResource("request-fields-with-title")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.setContent("{\"a\": \"foo\"}".getBytes()); + MockHttpServletResponse response = new MockHttpServletResponse(); + new RequestFieldsSnippet(attributes(key("title").value( + "Custom title")), Arrays.asList(fieldWithPath("a").description("one"))) + .document("request-fields-with-custom-attributes", + result(request, response)); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java new file mode 100644 index 000000000..9b165a4e9 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java @@ -0,0 +1,156 @@ +/* + * 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.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link PayloadDocumentation} + * + * @author Andy Wilkinson + */ +public class ResponseFieldsDocumentationHandlerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void mapResponseWithFields() throws IOException { + this.snippet.expectResponseFields("map-response-with-fields").withContents(// + tableWithHeader("Path", "Type", "Description") // + .row("id", "Number", "one") // + .row("date", "String", "two") // + .row("assets", "Array", "three") // + .row("assets[]", "Object", "four") // + .row("assets[].id", "Number", "five") // + .row("assets[].name", "String", "six")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}"); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id") + .description("one"), fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three"), fieldWithPath("assets[]") + .description("four"), + fieldWithPath("assets[].id").description("five"), + fieldWithPath("assets[].name").description("six"))).document( + "map-response-with-fields", result(response)); + } + + @Test + public void arrayResponseWithFields() throws IOException { + this.snippet.expectResponseFields("array-response-with-fields").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]a.b", "Number", "one") // + .row("[]a.c", "String", "two") // + .row("[]a", "Object", "three")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter() + .append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b") + .description("one"), fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))).document( + "array-response-with-fields", result(response)); + } + + @Test + public void arrayResponse() throws IOException { + this.snippet.expectResponseFields("array-response").withContents( // + tableWithHeader("Path", "Type", "Description") // + .row("[]", "String", "one")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("[\"a\", \"b\", \"c\"]"); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]") + .description("one"))).document("array-response", result(response)); + } + + @Test + public void responseFieldsWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("response-fields")).thenReturn( + snippetResource("response-fields-with-extra-column")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"); + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("a.b").description("one").attributes( + key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes( + key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes( + key("foo").value("charlie")))).document( + "response-fields-with-custom-attributes", result(request, response)); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("response-fields")).thenReturn( + snippetResource("response-fields-with-title")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getOutputStream().print("{\"a\": \"foo\"}"); + new ResponseFieldsSnippet(attributes(key("title").value( + "Custom title")), Arrays.asList(fieldWithPath("a").description("one"))) + .document("response-fields-with-custom-attributes", + result(request, response)); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java new file mode 100644 index 000000000..b980ead2b --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java @@ -0,0 +1,147 @@ +/* + * 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.request; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.restdocs.test.TestRequestBuilders.get; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link PathParametersSnippet} + * + * @author awilkinson + * + */ +public class PathParametersDocumentationHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void undocumentedPathParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [a]")); + new PathParametersSnippet(Collections. emptyList()) + .document("undocumented-path-parameter", result(get("/{a}/", "alpha"))); + } + + @Test + public void missingPathParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not found in the request: [a]")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))).document( + "missing-path-parameter", result(get("/"))); + } + + @Test + public void undocumentedAndMissingPathParameters() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [b]. Path parameters with the following" + + " names were not found in the request: [a]")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))).document( + "undocumented-and-missing-path-parameters", result(get("/{b}", "bravo"))); + } + + @Test + public void pathParameters() throws IOException { + this.snippet.expectPathParameters("path-parameters").withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row("b", + "two")); + new PathParametersSnippet(Arrays.asList( + parameterWithName("a").description("one"), parameterWithName("b") + .description("two"))).document( + "path-parameters", + result(get("/{a}/{b}", "alpha", "banana").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext(null)))); + } + + @Test + public void pathParametersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectPathParameters( + "path-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("path-parameters")).thenReturn( + snippetResource("path-parameters-with-extra-column")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), parameterWithName("b") + .description("two").attributes(key("foo").value("bravo")))).document( + "path-parameters-with-custom-descriptor-attributes", + result(get("{a}/{b}", "alpha", "bravo").requestAttr( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)))); + } + + @Test + public void pathParametersWithCustomAttributes() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("path-parameters")).thenReturn( + snippetResource("path-parameters-with-title")); + new PathParametersSnippet( + attributes(key("title").value("The title")), + Arrays.asList( + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), parameterWithName("b") + .description("two").attributes(key("foo").value("bravo")))) + .document( + "path-parameters-with-custom-attributes", + result(get("{a}/{b}", "alpha", "bravo").requestAttr( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)))); + + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java new file mode 100644 index 000000000..00fb20a76 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java @@ -0,0 +1,169 @@ +/* + * 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.request; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.StubMvcResult.result; +import static org.springframework.restdocs.test.TestRequestBuilders.get; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; + +/** + * Tests for {@link QueryParametersSnippet} + * + * @author Andy Wilkinson + */ +public class QueryParametersDocumentationHandlerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void undocumentedQueryParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not documented: [a]")); + new QueryParametersSnippet(Collections. emptyList()) + .document("undocumented-query-parameter", + result(get("/").param("a", "alpha"))); + } + + @Test + public void missingQueryParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not found in the request: [a]")); + new QueryParametersSnippet(Arrays.asList(parameterWithName("a") + .description("one"))).document("missing-query-parameter", + result(get("/"))); + } + + @Test + public void undocumentedAndMissingQueryParameters() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Query parameters with the following names were" + + " not documented: [b]. Query parameters with the following" + + " names were not found in the request: [a]")); + new QueryParametersSnippet(Arrays.asList(parameterWithName("a") + .description("one"))).document( + "undocumented-and-missing-query-parameters", + result(get("/").param("b", "bravo"))); + } + + @Test + public void queryParameterSnippetFromRequestParameters() throws IOException { + this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row( + "b", "two")); + new QueryParametersSnippet(Arrays.asList(parameterWithName("a") + .description("one"), parameterWithName("b").description("two"))) + .document("query-parameter-snippet-request-parameters", result(get("/") + .param("a", "bravo").param("b", "bravo"))); + } + + @Test + public void queryParameterSnippetFromRequestUriQueryString() throws IOException { + this.snippet.expectQueryParameters( + "query-parameter-snippet-request-uri-query-string").withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row("b", + "two")); + new QueryParametersSnippet(Arrays.asList(parameterWithName("a") + .description("one"), parameterWithName("b").description("two"))) + .document( + "query-parameter-snippet-request-uri-query-string", + result(get("/?a=alpha&b=bravo").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext(null)))); + } + + @Test + public void queryParametersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectQueryParameters( + "query-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("query-parameters")).thenReturn( + snippetResource("query-parameters-with-extra-column")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.addParameter("a", "bravo"); + request.addParameter("b", "bravo"); + new QueryParametersSnippet(Arrays.asList( + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo")))).document( + "query-parameters-with-custom-descriptor-attributes", result(request)); + } + + @Test + public void queryParametersWithCustomAttributes() throws IOException { + this.snippet.expectQueryParameters("query-parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("query-parameters")).thenReturn( + snippetResource("query-parameters-with-title")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + request.addParameter("a", "bravo"); + request.addParameter("b", "bravo"); + new QueryParametersSnippet( + attributes(key("title").value("The title")), + Arrays.asList( + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), parameterWithName("b") + .description("two").attributes(key("foo").value("bravo")))) + .document("query-parameters-with-custom-attributes", result(request)); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java deleted file mode 100644 index b193ad5c0..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ /dev/null @@ -1,252 +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.request; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.restdocs.test.TestRequestBuilders.get; - -import java.io.IOException; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.config.RestDocumentationContext; -import org.springframework.restdocs.snippet.SnippetGenerationException; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; - -/** - * Requests for {@link RequestDocumentation} - * - * @author Andy Wilkinson - */ -public class RequestDocumentationTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Test - public void undocumentedQueryParameter() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(equalTo("Query parameters with the following names were" - + " not documented: [a]")); - documentQueryParameters("undocumented-query-parameter", null).handle( - result(get("/").param("a", "alpha"))); - } - - @Test - public void missingQueryParameter() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(equalTo("Query parameters with the following names were" - + " not found in the request: [a]")); - documentQueryParameters("missing-query-parameter", null, - parameterWithName("a").description("one")).handle(result(get("/"))); - } - - @Test - public void undocumentedAndMissingQueryParameters() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown - .expectMessage(equalTo("Query parameters with the following names were" - + " not documented: [b]. Query parameters with the following" - + " names were not found in the request: [a]")); - documentQueryParameters("undocumented-and-missing-query-parameters", null, - parameterWithName("a").description("one")).handle( - result(get("/").param("b", "bravo"))); - } - - @Test - public void queryParameterSnippetFromRequestParameters() throws IOException { - this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters") - .withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row( - "b", "two")); - documentQueryParameters("query-parameter-snippet-request-parameters", null, - parameterWithName("a").description("one"), - parameterWithName("b").description("two")).handle( - result(get("/").param("a", "bravo").param("b", "bravo"))); - } - - @Test - public void queryParameterSnippetFromRequestUriQueryString() throws IOException { - this.snippet.expectQueryParameters( - "query-parameter-snippet-request-uri-query-string").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); - documentQueryParameters("query-parameter-snippet-request-uri-query-string", null, - parameterWithName("a").description("one"), - parameterWithName("b").description("two")).handle( - result(get("/?a=alpha&b=bravo").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext()))); - } - - @Test - public void queryParametersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectQueryParameters( - "query-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("query-parameters")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.addParameter("a", "bravo"); - request.addParameter("b", "bravo"); - documentQueryParameters( - "query-parameters-with-custom-descriptor-attributes", - null, - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))).handle(result(request)); - } - - @Test - public void queryParametersWithCustomAttributes() throws IOException { - this.snippet.expectQueryParameters("query-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("query-parameters")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.addParameter("a", "bravo"); - request.addParameter("b", "bravo"); - documentQueryParameters( - "query-parameters-with-custom-attributes", - attributes(key("title").value("The title")), - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))).handle(result(request)); - - } - - @Test - public void undocumentedPathParameter() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [a]")); - documentPathParameters("undocumented-path-parameter", null).handle( - result(get("/{a}/", "alpha"))); - } - - @Test - public void missingPathParameter() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not found in the request: [a]")); - documentPathParameters("missing-path-parameter", null, - parameterWithName("a").description("one")).handle(result(get("/"))); - } - - @Test - public void undocumentedAndMissingPathParameters() throws IOException { - this.thrown.expect(SnippetGenerationException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [b]. Path parameters with the following" - + " names were not found in the request: [a]")); - documentPathParameters("undocumented-and-missing-path-parameters", null, - parameterWithName("a").description("one")).handle( - result(get("/{b}", "bravo"))); - } - - @Test - public void pathParameters() throws IOException { - this.snippet.expectPathParameters("path-parameters").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); - documentPathParameters("path-parameters", null, - parameterWithName("a").description("one"), - parameterWithName("b").description("two")).handle( - result(get("/{a}/{b}", "alpha", "banana").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext()))); - } - - @Test - public void pathParametersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectPathParameters( - "path-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("path-parameters")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet")); - documentPathParameters( - "path-parameters-with-custom-descriptor-attributes", - null, - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))).handle( - result(get("{a}/{b}", "alpha", "bravo").requestAttr( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)))); - } - - @Test - public void pathParametersWithCustomAttributes() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("path-parameters")) - .thenReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet")); - documentPathParameters( - "path-parameters-with-custom-attributes", - attributes(key("title").value("The title")), - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))).handle( - result(get("{a}/{b}", "alpha", "bravo").requestAttr( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)))); - - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 37d8fa444..c1882d27e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -24,6 +24,7 @@ import java.io.File; import org.junit.Test; +import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; /** diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 5847424c5..c1e958142 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -26,12 +26,12 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; /** * The {@code ExpectedSnippet} rule is used to verify that a - * {@link SnippetWritingResultHandler} has generated the expected snippet. + * {@link TemplatedSnippet} has generated the expected snippet. * * @author Andy Wilkinson */ diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index d772f7f0a..284f24d89 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -92,7 +92,7 @@ private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse re this.request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(new StandardTemplateResourceResolver())); } - RestDocumentationContext context = new RestDocumentationContext(); + RestDocumentationContext context = new RestDocumentationContext(null); this.request.setAttribute(RestDocumentationContext.class.getName(), context); if (this.request.getAttribute(WriterResolver.class.getName()) == null) { this.request.setAttribute(WriterResolver.class.getName(), diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java index de6d5df0c..fa877f5b8 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/TestRequestBuilders.java @@ -29,7 +29,8 @@ public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { return org.springframework.restdocs.RestDocumentationRequestBuilders.get( urlTemplate, urlVariables).requestAttr( - RestDocumentationContext.class.getName(), new RestDocumentationContext()); + RestDocumentationContext.class.getName(), + new RestDocumentationContext(null)); } } From dfd65f9f103d7927948d6b9459ab15d384c2ebc4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Aug 2015 09:15:50 +0100 Subject: [PATCH 0108/1059] Polishing: rename test classes following refactoring in 0c8a7137 --- ...umentationHandlerTests.java => CurlRequestSnippetTests.java} | 2 +- ...umentationHandlerTests.java => HttpRequestSnippetTests.java} | 2 +- ...mentationHandlerTests.java => HttpResponseSnippetTests.java} | 2 +- ...nksDocumentationHandlerTests.java => LinksSnippetTests.java} | 2 +- ...entationHandlerTests.java => RequestFieldsSnippetTests.java} | 2 +- ...ntationHandlerTests.java => ResponseFieldsSnippetTests.java} | 2 +- ...ntationHandlerTests.java => PathParametersSnippetTests.java} | 2 +- ...tationHandlerTests.java => QueryParametersSnippetTests.java} | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename spring-restdocs/src/test/java/org/springframework/restdocs/curl/{CurlRequestDocumentationHandlerTests.java => CurlRequestSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/http/{HttpRequestDocumentationHandlerTests.java => HttpRequestSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/http/{HttpResponseDocumentationHandlerTests.java => HttpResponseSnippetTests.java} (98%) rename spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/{LinksDocumentationHandlerTests.java => LinksSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/payload/{RequestFieldsDocumentationHandlerTests.java => RequestFieldsSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/payload/{ResponseFieldsDocumentationHandlerTests.java => ResponseFieldsSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/request/{PathParametersDocumentationHandlerTests.java => PathParametersSnippetTests.java} (99%) rename spring-restdocs/src/test/java/org/springframework/restdocs/request/{QueryParametersDocumentationHandlerTests.java => QueryParametersSnippetTests.java} (99%) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 300cf6fa8..b1cec398c 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -50,7 +50,7 @@ * @author Dmitriy Mayboroda * @author Jonathan Pearlin */ -public class CurlRequestDocumentationHandlerTests { +public class CurlRequestSnippetTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index beb799979..4d5693393 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -52,7 +52,7 @@ * @author Jonathan Pearlin * */ -public class HttpRequestDocumentationHandlerTests { +public class HttpRequestSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java similarity index 98% rename from spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index c9983761c..f9f7bac5e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -45,7 +45,7 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpResponseDocumentationHandlerTests { +public class HttpResponseSnippetTests { @Rule public final ExpectedSnippet snippet = new ExpectedSnippet(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 71943b109..99ad2bc60 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -48,7 +48,7 @@ * * @author Andy Wilkinson */ -public class LinksDocumentationHandlerTests { +public class LinksSnippetTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 4e7722601..d81ce320a 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -49,7 +49,7 @@ * * @author Andy Wilkinson */ -public class RequestFieldsDocumentationHandlerTests { +public class RequestFieldsSnippetTests { @Rule public final ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 9b165a4e9..b39802b53 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -43,7 +43,7 @@ * * @author Andy Wilkinson */ -public class ResponseFieldsDocumentationHandlerTests { +public class ResponseFieldsSnippetTests { @Rule public final ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index b980ead2b..1ba26c2b6 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -48,7 +48,7 @@ * @author awilkinson * */ -public class PathParametersDocumentationHandlerTests { +public class PathParametersSnippetTests { @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java similarity index 99% rename from spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java index 00fb20a76..fe0efd901 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersDocumentationHandlerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java @@ -48,7 +48,7 @@ * * @author Andy Wilkinson */ -public class QueryParametersDocumentationHandlerTests { +public class QueryParametersSnippetTests { @Rule public ExpectedException thrown = ExpectedException.none(); From d3e1a0d1b6b74f09b3d03e4c927b84069759ed2d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Aug 2015 10:07:39 +0100 Subject: [PATCH 0109/1059] Provide obvious support for documenting all request parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the snippet for documenting a request’s parameters was referred to as the query parameters snippet. This was misleading as the snippet would actually document anything in the request’s parameters map, not just parameters from the request’s query string. This commit renames the snippet and the associated templates, etc so that it is now known as the request parameters snippet. This provides a more accurate reflection of it being able to document all of a request’s parameters, not just those from its query string. Closes gh-104 --- .../docs/asciidoc/documenting-your-api.adoc | 36 ++++--- ...Parameters.java => RequestParameters.java} | 29 +++-- .../restdocs/request/ParameterDescriptor.java | 2 +- .../request/RequestDocumentation.java | 58 +++++----- ...pet.java => RequestParametersSnippet.java} | 22 ++-- ...pet => default-request-parameters.snippet} | 0 .../RestDocumentationIntegrationTests.java | 8 +- ...ava => RequestParametersSnippetTests.java} | 100 +++++++++--------- .../restdocs/test/ExpectedSnippet.java | 8 +- ...uest-parameters-with-extra-column.snippet} | 0 ... => request-parameters-with-title.snippet} | 0 11 files changed, 148 insertions(+), 115 deletions(-) rename docs/src/test/java/com/example/{QueryParameters.java => RequestParameters.java} (61%) rename spring-restdocs/src/main/java/org/springframework/restdocs/request/{QueryParametersSnippet.java => RequestParametersSnippet.java} (66%) rename spring-restdocs/src/main/resources/org/springframework/restdocs/templates/{default-query-parameters.snippet => default-request-parameters.snippet} (100%) rename spring-restdocs/src/test/java/org/springframework/restdocs/request/{QueryParametersSnippetTests.java => RequestParametersSnippetTests.java} (57%) rename spring-restdocs/src/test/resources/custom-snippet-templates/{query-parameters-with-extra-column.snippet => request-parameters-with-extra-column.snippet} (100%) rename spring-restdocs/src/test/resources/custom-snippet-templates/{query-parameters-with-title.snippet => request-parameters-with-title.snippet} (100%) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 0018d7d0b..908a5fd87 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -174,27 +174,37 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type] -[[documenting-your-api-query-parameters]] -=== Query parameters +[[documenting-your-api-request-parameters]] +=== Request parameters -A request's query parameters can be documented using `queryParameters` +A request's parameters can be documented using `requestParameters`. Request parameters +can be included in a `GET` requests query string: [source,java,indent=0] ---- -include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters] +include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-query-string] ---- -<1> Produce a snippet describing the request's query parameters. Uses the static -`queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document a parameter named `page`. Uses the static `parameterWithName` method on +<1> Perform a `GET` request with two parameters in the query string. +<2> Produce a snippet describing the request's parameters. Uses the static +`requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document a parameter named `page`. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document a parameter named `per_page`. +<4> Document a parameter named `per_page`. -The result is a snippet named `query-parameters.adoc` that contains a table describing -the query parameters that are supported by the resource. +They can also be included as form data in the body of a POST request: -When documenting query parameters, the test will fail if an undocumented query parameter -is used in the request. Similarly, the test will also fail if a documented query parameter -is not found in the request. +[source,java,indent=0] +---- +include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-form-data] +---- +<1> Perform a `POST` request with a single parameter. + +In both cases, the result is a snippet named `request-parameters.adoc` that contains a +table describing the parameters that are supported by the resource. + +When documenting request parameters, the test will fail if an undocumented request +parameter is used in the request. Similarly, the test will also fail if a documented +request parameter is not found in the request. diff --git a/docs/src/test/java/com/example/QueryParameters.java b/docs/src/test/java/com/example/RequestParameters.java similarity index 61% rename from docs/src/test/java/com/example/QueryParameters.java rename to docs/src/test/java/com/example/RequestParameters.java index 4afcc35dc..62e882917 100644 --- a/docs/src/test/java/com/example/QueryParameters.java +++ b/docs/src/test/java/com/example/RequestParameters.java @@ -18,25 +18,36 @@ import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.MockMvc; -public class QueryParameters { +public class RequestParameters { private MockMvc mockMvc; - public void queryParametersSnippet() throws Exception { - // tag::query-parameters[] - this.mockMvc.perform(get("/users?page=2&per_page=100")) + public void getQueryStringSnippet() throws Exception { + // tag::request-parameters-query-string[] + this.mockMvc.perform(get("/users?page=2&per_page=100")) // <1> .andExpect(status().isOk()) - .andDo(document("users", queryParameters( // <1> - parameterWithName("page").description("The page to retrieve"), // <2> - parameterWithName("per_page").description("Entries per page") // <3> + .andDo(document("users", requestParameters( // <2> + parameterWithName("page").description("The page to retrieve"), // <3> + parameterWithName("per_page").description("Entries per page") // <4> ))); - // end::query-parameters[] + // end::request-parameters-query-string[] + } + + public void postFormDataSnippet() throws Exception { + // tag::request-parameters-form-data[] + this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> + .andExpect(status().isCreated()) + .andDo(document("create-user", requestParameters( + parameterWithName("username").description("The user's username") + ))); + // end::request-parameters-form-data[] } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 3eb82ba20..8edaf3ccf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -22,7 +22,7 @@ import org.springframework.restdocs.snippet.AbstractDescriptor; /** - * A descriptor of a parameter in a query string + * A descriptor of a request or path parameter * * @author Andy Wilkinson * @see RequestDocumentation#parameterWithName diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index fea071e32..9f3887f2e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -19,7 +19,11 @@ import java.util.Arrays; import java.util.Map; +import javax.servlet.ServletRequest; + import org.springframework.restdocs.snippet.Snippet; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -33,8 +37,8 @@ private RequestDocumentation() { } /** - * Creates a {@link ParameterDescriptor} that describes a query string parameter with - * the given {@code name}. + * Creates a {@link ParameterDescriptor} that describes a request or path parameter + * with the given {@code name}. * * @param name The name of the parameter * @return a {@link ParameterDescriptor} ready for further configuration @@ -44,57 +48,59 @@ public static ParameterDescriptor parameterWithName(String name) { } /** - * Returns a handler that will produce a snippet documenting the path parameters from - * the API call's request. + * Returns a snippet that will document the path parameters from the API call's + * request. * * @param descriptors The descriptions of the parameters in the request's path - * @return the handler + * @return the snippet + * @see PathVariable */ public static Snippet pathParameters(ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a handler that will produce a snippet documenting the path parameters from - * the API call's request. The given {@code attributes} will be available during - * snippet generation. + * Returns a snippet that will document the path parameters from the API call's + * request. The given {@code attributes} will be available during snippet rendering. * * @param attributes Attributes made available during rendering of the path parameters * snippet * @param descriptors The descriptions of the parameters in the request's path - * @return the handler + * @return the snippet + * @see PathVariable */ public static Snippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(attributes, - Arrays.asList(descriptors)); + return new PathParametersSnippet(attributes, Arrays.asList(descriptors)); } /** - * Returns a handler that will produce a snippet documenting the query parameters from - * the API call's request. + * Returns a snippet that will document the request parameters from the API call's + * request. * - * @param descriptors The descriptions of the request's query parameters - * @return the handler + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see RequestParam + * @see ServletRequest#getParameterMap() */ - public static Snippet queryParameters(ParameterDescriptor... descriptors) { - return new QueryParametersSnippet(Arrays.asList(descriptors)); + public static Snippet requestParameters(ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a handler that will produce a snippet documenting the query parameters from - * the API call's request. The given {@code attributes} will be available during - * snippet generation. + * Returns a snippet that will document the request parameters from the API call's + * request. The given {@code attributes} will be available during snippet rendering. * - * @param attributes Attributes made available during rendering of the query + * @param attributes Attributes made available during rendering of the request * parameters snippet - * @param descriptors The descriptions of the request's query parameters - * @return the handler + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see RequestParam + * @see ServletRequest#getParameterMap() */ - public static Snippet queryParameters(Map attributes, + public static Snippet requestParameters(Map attributes, ParameterDescriptor... descriptors) { - return new QueryParametersSnippet(attributes, - Arrays.asList(descriptors)); + return new RequestParametersSnippet(attributes, Arrays.asList(descriptors)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java similarity index 66% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 0bd11b88d..1916c9b69 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -20,24 +20,32 @@ import java.util.Map; import java.util.Set; +import javax.servlet.ServletRequest; + import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.RequestParam; /** - * A {@link Snippet} that documents the query parameters supported by a RESTful resource. + * A {@link Snippet} that documents the request parameters supported by a RESTful + * resource. + *

+ * Request parameters are sent as part of the query string or as posted from data. * * @author Andy Wilkinson + * @see ServletRequest#getParameterMap() + * @see RequestParam */ -class QueryParametersSnippet extends AbstractParametersSnippet { +class RequestParametersSnippet extends AbstractParametersSnippet { - QueryParametersSnippet(List descriptors) { + RequestParametersSnippet(List descriptors) { this(null, descriptors); } - QueryParametersSnippet(Map attributes, + RequestParametersSnippet(Map attributes, List descriptors) { - super("query-parameters", attributes, descriptors); + super("request-parameters", attributes, descriptors); } @Override @@ -45,14 +53,14 @@ protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { String message = ""; if (!undocumentedParameters.isEmpty()) { - message += "Query parameters with the following names were not documented: " + message += "Request parameters with the following names were not documented: " + undocumentedParameters; } if (!missingParameters.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Query parameters with the following names were not found in the request: " + message += "Request parameters with the following names were not found in the request: " + missingParameters; } throw new SnippetException(message); diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet rename to spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index f3642d76a..7f8cb279e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -31,7 +31,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; @@ -148,18 +148,18 @@ public void pathParametersSnippet() throws Exception { } @Test - public void queryParametersSnippet() throws Exception { + public void requestParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", queryParameters(parameterWithName("foo") + .andDo(document("links", requestParameters(parameterWithName("foo") .description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "query-parameters.adoc"); + "request-parameters.adoc"); } @Test diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java similarity index 57% rename from spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index fe0efd901..fd74544dd 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -44,11 +44,11 @@ import org.springframework.restdocs.test.ExpectedSnippet; /** - * Tests for {@link QueryParametersSnippet} + * Tests for {@link RequestParametersSnippet} * * @author Andy Wilkinson */ -public class QueryParametersSnippetTests { +public class RequestParametersSnippetTests { @Rule public ExpectedException thrown = ExpectedException.none(); @@ -57,108 +57,106 @@ public class QueryParametersSnippetTests { public ExpectedSnippet snippet = new ExpectedSnippet(); @Test - public void undocumentedQueryParameter() throws IOException { + public void undocumentedParameter() throws IOException { this.thrown.expect(SnippetException.class); this.thrown - .expectMessage(equalTo("Query parameters with the following names were" + .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); - new QueryParametersSnippet(Collections. emptyList()) - .document("undocumented-query-parameter", - result(get("/").param("a", "alpha"))); + new RequestParametersSnippet(Collections. emptyList()) + .document("undocumented-parameter", result(get("/").param("a", "alpha"))); } @Test - public void missingQueryParameter() throws IOException { + public void missingParameter() throws IOException { this.thrown.expect(SnippetException.class); this.thrown - .expectMessage(equalTo("Query parameters with the following names were" + .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); - new QueryParametersSnippet(Arrays.asList(parameterWithName("a") - .description("one"))).document("missing-query-parameter", - result(get("/"))); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document("missing-parameter", result(get("/"))); } @Test - public void undocumentedAndMissingQueryParameters() throws IOException { + public void undocumentedAndMissingParameters() throws IOException { this.thrown.expect(SnippetException.class); this.thrown - .expectMessage(equalTo("Query parameters with the following names were" - + " not documented: [b]. Query parameters with the following" + .expectMessage(equalTo("Request parameters with the following names were" + + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); - new QueryParametersSnippet(Arrays.asList(parameterWithName("a") - .description("one"))).document( - "undocumented-and-missing-query-parameters", - result(get("/").param("b", "bravo"))); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document("undocumented-and-missing-parameters", result(get("/") + .param("b", "bravo"))); } @Test - public void queryParameterSnippetFromRequestParameters() throws IOException { - this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters") - .withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row( - "b", "two")); - new QueryParametersSnippet(Arrays.asList(parameterWithName("a") - .description("one"), parameterWithName("b").description("two"))) - .document("query-parameter-snippet-request-parameters", result(get("/") - .param("a", "bravo").param("b", "bravo"))); + public void requestParameterSnippetFromRequestParameters() throws IOException { + this.snippet.expectRequestParameters( + "request-parameter-snippet-request-parameters").withContents( + tableWithHeader("Parameter", "Description").row("a", "one").row("b", + "two")); + new RequestParametersSnippet(Arrays.asList( + parameterWithName("a").description("one"), parameterWithName("b") + .description("two"))).document( + "request-parameter-snippet-request-parameters", + result(get("/").param("a", "bravo").param("b", "bravo"))); } @Test - public void queryParameterSnippetFromRequestUriQueryString() throws IOException { - this.snippet.expectQueryParameters( - "query-parameter-snippet-request-uri-query-string").withContents( + public void requestParameterSnippetFromRequestUriQueryString() throws IOException { + this.snippet.expectRequestParameters( + "request-parameter-snippet-request-uri-query-string").withContents( tableWithHeader("Parameter", "Description").row("a", "one").row("b", "two")); - new QueryParametersSnippet(Arrays.asList(parameterWithName("a") - .description("one"), parameterWithName("b").description("two"))) - .document( - "query-parameter-snippet-request-uri-query-string", - result(get("/?a=alpha&b=bravo").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext(null)))); + new RequestParametersSnippet(Arrays.asList( + parameterWithName("a").description("one"), parameterWithName("b") + .description("two"))).document( + "request-parameter-snippet-request-uri-query-string", + result(get("/?a=alpha&b=bravo").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext(null)))); } @Test - public void queryParametersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectQueryParameters( - "query-parameters-with-custom-descriptor-attributes").withContents( + public void requestParametersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestParameters( + "request-parameters-with-custom-descriptor-attributes").withContents( tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("query-parameters")).thenReturn( - snippetResource("query-parameters-with-extra-column")); + when(resolver.resolveTemplateResource("request-parameters")).thenReturn( + snippetResource("request-parameters-with-extra-column")); MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); request.addParameter("a", "bravo"); request.addParameter("b", "bravo"); - new QueryParametersSnippet(Arrays.asList( + new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( key("foo").value("bravo")))).document( - "query-parameters-with-custom-descriptor-attributes", result(request)); + "request-parameters-with-custom-descriptor-attributes", result(request)); } @Test - public void queryParametersWithCustomAttributes() throws IOException { - this.snippet.expectQueryParameters("query-parameters-with-custom-attributes") + public void requestParametersWithCustomAttributes() throws IOException { + this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") .withContents(startsWith(".The title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("query-parameters")).thenReturn( - snippetResource("query-parameters-with-title")); + when(resolver.resolveTemplateResource("request-parameters")).thenReturn( + snippetResource("request-parameters-with-title")); MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); request.addParameter("a", "bravo"); request.addParameter("b", "bravo"); - new QueryParametersSnippet( + new RequestParametersSnippet( attributes(key("title").value("The title")), Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document("query-parameters-with-custom-attributes", result(request)); + .document("request-parameters-with-custom-attributes", result(request)); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index c1e958142..c3b72eb3b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -30,8 +30,8 @@ import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; /** - * The {@code ExpectedSnippet} rule is used to verify that a - * {@link TemplatedSnippet} has generated the expected snippet. + * The {@code ExpectedSnippet} rule is used to verify that a {@link TemplatedSnippet} has + * generated the expected snippet. * * @author Andy Wilkinson */ @@ -130,8 +130,8 @@ public ExpectedSnippet expectHttpResponse(String name) { return this; } - public ExpectedSnippet expectQueryParameters(String name) { - expect(name, "query-parameters"); + public ExpectedSnippet expectRequestParameters(String name) { + expect(name, "request-parameters"); return this; } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet rename to spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet rename to spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet From 76dd0cc5795fa39bce4651dccd30c26e17f5fd1e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Aug 2015 11:33:42 +0100 Subject: [PATCH 0110/1059] =?UTF-8?q?Include=20request=E2=80=99s=20path=20?= =?UTF-8?q?(without=20query=20string)=20in=20path=20parameters=20snippet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ordering of parameters in the path is important so, without seeing the path and the order in which the parameters appear, documentation for the individual parameters is of limited use. This commit enhances the path parameters snippet to include the request’s path, minus any query string, in the snippet. By default, the path is rendered as the title of the parameters table. As with other snippets, this can be customized by providing a custom template for the snippet. Closes gh-103 --- .../request/PathParametersSnippet.java | 16 +++++++++++++++ .../templates/default-path-parameters.snippet | 1 + .../request/PathParametersSnippetTests.java | 20 +++++++++++++++++-- .../restdocs/test/SnippetMatchers.java | 12 +++++++++-- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 91bf624d2..a8ae9d2af 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.request; +import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -46,6 +47,21 @@ class PathParametersSnippet extends AbstractParametersSnippet { super("path-parameters", attributes, descriptors); } + @Override + protected Map document(MvcResult result) throws IOException { + Map model = super.document(result); + model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(result))); + return model; + } + + private String remoteQueryStringIfPresent(String urlTemplate) { + int index = urlTemplate.indexOf('?'); + if (index == -1) { + return urlTemplate; + } + return urlTemplate.substring(0, index); + } + @Override protected Set extractActualParameters(MvcResult result) { String urlTemplate = extractUrlTemplate(result); diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet index 067e1fb83..66db38621 100644 --- a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet @@ -1,3 +1,4 @@ +.{{path}} |=== |Parameter|Description diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 1ba26c2b6..43ed107b6 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -24,6 +24,7 @@ import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.restdocs.test.TestRequestBuilders.get; @@ -89,8 +90,8 @@ public void undocumentedAndMissingPathParameters() throws IOException { @Test public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("a", + "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") .description("two"))).document( @@ -100,6 +101,21 @@ public void pathParameters() throws IOException { new RestDocumentationContext(null)))); } + @Test + public void pathParametersWithQueryString() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-query-string") + .withContents( + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + .row("a", "one").row("b", "two")); + new PathParametersSnippet(Arrays.asList( + parameterWithName("a").description("one"), parameterWithName("b") + .description("two"))).document( + "path-parameters-with-query-string", + result(get("/{a}/{b}?foo=bar", "alpha", "banana").requestAttr( + RestDocumentationContext.class.getName(), + new RestDocumentationContext(null)))); + } + @Test public void pathParametersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectPathParameters( diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 46eecfcd7..b174cf40d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -44,8 +44,13 @@ public static SnippetMatcher snippet() { return new SnippetMatcher(); } + public static AsciidoctorTableMatcher tableWithTitleAndHeader(String title, + String... headers) { + return new AsciidoctorTableMatcher(title, headers); + } + public static AsciidoctorTableMatcher tableWithHeader(String... headers) { - return new AsciidoctorTableMatcher(headers); + return new AsciidoctorTableMatcher(null, headers); } public static HttpRequestMatcher httpRequest(RequestMethod method, String uri) { @@ -166,7 +171,10 @@ public HttpRequestMatcher(RequestMethod requestMethod, String uri) { public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher { - private AsciidoctorTableMatcher(String... columns) { + private AsciidoctorTableMatcher(String title, String... columns) { + if (StringUtils.hasText(title)) { + this.addLine("." + title); + } this.addLine("|==="); String header = "|" + StringUtils From 3fd65791d184fe167edb2a7dc706d76dd5f8c7bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 19 Aug 2015 16:24:29 +0100 Subject: [PATCH 0111/1059] Add support for documenting fields in XML payloads Closes gh-46 --- .../docs/asciidoc/documenting-your-api.adoc | 43 ++++-- docs/src/test/java/com/example/Payload.java | 4 +- .../payload/AbstractFieldsSnippet.java | 96 +++++++----- .../restdocs/payload/FieldDescriptor.java | 11 +- .../payload/FieldDoesNotExistException.java | 2 +- .../restdocs/payload/FieldValidator.java | 98 ------------ .../{FieldPath.java => JsonFieldPath.java} | 10 +- ...Processor.java => JsonFieldProcessor.java} | 20 +-- .../{FieldType.java => JsonFieldType.java} | 2 +- ...solver.java => JsonFieldTypeResolver.java} | 30 ++-- .../restdocs/payload/JsonPayloadHandler.java | 92 ++++++++++++ .../payload/PayloadDocumentation.java | 14 +- .../restdocs/payload/PayloadHandler.java | 45 ++++++ .../payload/PayloadHandlingException.java | 20 +++ .../payload/RequestFieldsSnippet.java | 10 ++ .../payload/ResponseFieldsSnippet.java | 12 +- .../restdocs/payload/XmlPayloadHandler.java | 141 ++++++++++++++++++ .../restdocs/payload/FieldValidatorTests.java | 114 -------------- ...PathTests.java => JsonFieldPathTests.java} | 34 ++--- ...ests.java => JsonFieldProcessorTests.java} | 44 +++--- ...s.java => JsonFieldTypeResolverTests.java} | 26 ++-- .../payload/RequestFieldsSnippetTests.java | 107 ++++++++++--- .../payload/ResponseFieldsSnippetTests.java | 103 +++++++++++-- 23 files changed, 684 insertions(+), 394 deletions(-) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{FieldPath.java => JsonFieldPath.java} (90%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{FieldProcessor.java => JsonFieldProcessor.java} (90%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{FieldType.java => JsonFieldType.java} (97%) rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{FieldTypeResolver.java => JsonFieldTypeResolver.java} (66%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/payload/{FieldPathTests.java => JsonFieldPathTests.java} (67%) rename spring-restdocs/src/test/java/org/springframework/restdocs/payload/{FieldProcessorTests.java => JsonFieldProcessorTests.java} (80%) rename spring-restdocs/src/test/java/org/springframework/restdocs/payload/{FieldTypeResolverTests.java => JsonFieldTypeResolverTests.java} (79%) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 908a5fd87..02a5d6976 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -81,12 +81,18 @@ payload and the field has not been marked as optional. For payloads with a hiera structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. -[[documenting-your-api-request-response-payloads-field-paths]] -==== Field paths +TIP: By default, Spring REST Docs will assume that the payload you are documenting is +JSON. If you want to document an XML payload the content type of the request or response +must be compatible with `application/xml`. -When documenting request and response payloads, fields are identified using a path. Paths -use `.` to descend into a child object and `[]` to identify an array. For example, with -this JSON payload: +[[documenting-your-api-request-response-payloads-json]] +==== JSON payloads + +[[documenting-your-api-request-response-payloads-json-field-paths]] +===== JSON field paths + +JSON field paths use `.` to descend into a child object and `[]` to identify an array. For +example, with this JSON payload: [source,json,indent=0] ---- @@ -131,8 +137,8 @@ The following paths are all present: -[[documenting-your-api-request-response-payloads-field-types]] -==== Field types +[[documenting-your-api-request-response-payloads-json-field-types]] +===== JSON field types When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: @@ -163,8 +169,9 @@ examining the payload. Seven different types are supported: | The field occurs multiple times in the payload with a variety of different types |=== -The type can also be set explicitly using the `type(FieldType)` method on -`FieldDescriptor`: +The type can also be set explicitly using the `type(Object)` method on +`FieldDescriptor`. Typically, one of the values enumerated by `JsonFieldType` will be +used: [source,java,indent=0] ---- @@ -173,6 +180,24 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type] <1> Set the field's type to `string`. +[[documenting-your-api-request-response-payloads-xml]] +==== XML payloads + +[[documenting-your-api-request-response-payloads-xml-field-paths]] +===== XML field paths + +XML field paths are described using XPath. `/` is used to descend into a child node. + + + +[[documenting-your-api-request-response-payloads-xml-field-types]] +===== XML field types + +When documenting an XML payload, you must provide a type for the field using the +`type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` +method will be included in the documentation. + + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index a317ec1cd..1ca668b8f 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -27,7 +27,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.FieldType; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; public class Payload { @@ -50,7 +50,7 @@ public void explicitType() throws Exception { // tag::explicit-type[] .andDo(document("index", responseFields( fieldWithPath("contact.email") - .type(FieldType.STRING) // <1> + .type(JsonFieldType.STRING) // <1> .optional() .description("The user's email address")))); // end::explicit-type[] diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index d9dd6df18..b8d4aa2dd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -18,36 +18,28 @@ import java.io.IOException; import java.io.Reader; +import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; - -import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; /** - * A {@link TemplatedSnippet} that produces a snippet documenting a - * RESTful resource's request or response fields. + * A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's + * request or response fields. * * @author Andreas Evers * @author Andy Wilkinson */ -public abstract class AbstractFieldsSnippet extends - TemplatedSnippet { - - private final Map descriptorsByPath = new LinkedHashMap(); - - private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver(); - - private final FieldValidator fieldValidator = new FieldValidator(); - - private final ObjectMapper objectMapper = new ObjectMapper(); +public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private List fieldDescriptors; @@ -57,46 +49,74 @@ public abstract class AbstractFieldsSnippet extends for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); - this.descriptorsByPath.put(descriptor.getPath(), descriptor); } this.fieldDescriptors = descriptors; } @Override protected Map document(MvcResult result) throws IOException { - this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - Object payload = extractPayload(result); + MediaType contentType = getContentType(result); + PayloadHandler payloadHandler; + if (contentType != null + && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { + payloadHandler = new XmlPayloadHandler(readPayload(result)); + } + else { + payloadHandler = new JsonPayloadHandler(readPayload(result)); + } + + validateFieldDocumentation(payloadHandler); + + for (FieldDescriptor descriptor : this.fieldDescriptors) { + if (descriptor.getType() == null) { + descriptor.type(payloadHandler.determineFieldType(descriptor.getPath())); + } + } + Map model = new HashMap<>(); List> fields = new ArrayList<>(); model.put("fields", fields); - for (Entry entry : this.descriptorsByPath.entrySet()) { - FieldDescriptor descriptor = entry.getValue(); - if (descriptor.getType() == null) { - descriptor.type(getFieldType(descriptor, payload)); - } + for (FieldDescriptor descriptor : this.fieldDescriptors) { fields.add(descriptor.toModel()); } return model; } - private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - try { - return AbstractFieldsSnippet.this.fieldTypeResolver - .resolveFieldType(descriptor.getPath(), payload); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the" - + " payload. Please provide a type using" - + " FieldDescriptor.type(FieldType)."; - throw new FieldTypeRequiredException(message); - } + private String readPayload(MvcResult result) throws IOException { + StringWriter writer = new StringWriter(); + FileCopyUtils.copy(getPayloadReader(result), writer); + return writer.toString(); } - private Object extractPayload(MvcResult result) throws IOException { - return this.objectMapper.readValue(getPayloadReader(result), Object.class); + private void validateFieldDocumentation(PayloadHandler payloadHandler) { + List missingFields = payloadHandler + .findMissingFields(this.fieldDescriptors); + String undocumentedPayload = payloadHandler + .getUndocumentedPayload(this.fieldDescriptors); + + if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { + String message = ""; + if (StringUtils.hasText(undocumentedPayload)) { + message += String.format("The following parts of the payload were" + + " not documented:%n%s", undocumentedPayload); + } + if (!missingFields.isEmpty()) { + if (message.length() > 0) { + message += String.format("%n"); + } + List paths = new ArrayList(); + for (FieldDescriptor fieldDescriptor : missingFields) { + paths.add(fieldDescriptor.getPath()); + } + message += "Fields with the following paths were not found in the" + + " payload: " + paths; + } + throw new SnippetException(message); + } } + protected abstract MediaType getContentType(MvcResult result); + 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/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 0a14327e3..5cbf63843 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -33,7 +33,7 @@ public class FieldDescriptor extends AbstractDescriptor { private final String path; - private FieldType type; + private Object type; private boolean optional; @@ -44,13 +44,14 @@ public class FieldDescriptor extends AbstractDescriptor { } /** - * Specifies the type of the field + * Specifies the type of the field. When documenting a JSON payload, the + * {@link JsonFieldType} enumeration will typically be used. * * @param type The type of the field - * * @return {@code this} + * @see JsonFieldType */ - public FieldDescriptor type(FieldType type) { + public FieldDescriptor type(Object type) { this.type = type; return this; } @@ -80,7 +81,7 @@ String getPath() { return this.path; } - FieldType getType() { + Object getType() { return this.type; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java index 99f565e23..e2cf49064 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java @@ -31,7 +31,7 @@ public class FieldDoesNotExistException extends RuntimeException { * * @param fieldPath the path of the field that does not exist */ - public FieldDoesNotExistException(FieldPath fieldPath) { + public FieldDoesNotExistException(JsonFieldPath fieldPath) { super("The payload does not contain a field with the path '" + fieldPath + "'"); } } 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 deleted file mode 100644 index 82e43717e..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.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.payload; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.springframework.restdocs.snippet.SnippetException; - -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 FieldProcessor fieldProcessor = new FieldProcessor(); - - private final ObjectMapper objectMapper = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT); - - void validate(Reader payloadReader, List fieldDescriptors) - throws IOException { - Object payload = this.objectMapper.readValue(payloadReader, Object.class); - List missingFields = findMissingFields(payload, fieldDescriptors); - Object undocumentedPayload = findUndocumentedFields(payload, fieldDescriptors); - - if (!missingFields.isEmpty() || !isEmpty(undocumentedPayload)) { - String message = ""; - if (!isEmpty(undocumentedPayload)) { - message += String.format( - "The following parts of the payload were not documented:%n%s", - this.objectMapper.writeValueAsString(undocumentedPayload)); - } - if (!missingFields.isEmpty()) { - if (message.length() > 0) { - message += String.format("%n"); - } - message += "Fields with the following paths were not found in the payload: " - + missingFields; - } - throw new SnippetException(message); - } - } - - private boolean isEmpty(Object object) { - if (object instanceof Map) { - return ((Map) object).isEmpty(); - } - return ((List) object).isEmpty(); - } - - private List findMissingFields(Object payload, - List fieldDescriptors) { - List missingFields = new ArrayList(); - - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional() - && !this.fieldProcessor.hasField( - FieldPath.compile(fieldDescriptor.getPath()), payload)) { - missingFields.add(fieldDescriptor.getPath()); - } - } - - return missingFields; - } - - private Object findUndocumentedFields(Object payload, - List fieldDescriptors) { - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - FieldPath path = FieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, payload); - } - return payload; - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java similarity index 90% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index a1dfb0339..ea44aaff2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldPath.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -22,12 +22,12 @@ import java.util.regex.Pattern; /** - * A path that identifies a field in a payload + * A path that identifies a field in a JSON payload * * @author Andy Wilkinson * */ -final class FieldPath { +final class JsonFieldPath { private static final Pattern ARRAY_INDEX_PATTERN = Pattern .compile("\\[([0-9]+|\\*){0,1}\\]"); @@ -38,7 +38,7 @@ final class FieldPath { private final boolean precise; - private FieldPath(String rawPath, List segments, boolean precise) { + private JsonFieldPath(String rawPath, List segments, boolean precise) { this.rawPath = rawPath; this.segments = segments; this.precise = precise; @@ -57,9 +57,9 @@ public String toString() { return this.rawPath; } - static FieldPath compile(String path) { + static JsonFieldPath compile(String path) { List segments = extractSegments(path); - return new FieldPath(path, segments, matchesSingleValue(segments)); + return new JsonFieldPath(path, segments, matchesSingleValue(segments)); } static boolean isArraySegment(String segment) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java similarity index 90% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index f0e3d25d2..e1aa72d69 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -23,15 +23,15 @@ import java.util.concurrent.atomic.AtomicReference; /** - * A {@code FieldProcessor} processes a payload's fields, allowing them to be extracted - * and removed + * A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be + * extracted and removed * * @author Andy Wilkinson * */ -final class FieldProcessor { +final class JsonFieldProcessor { - boolean hasField(FieldPath fieldPath, Object payload) { + boolean hasField(JsonFieldPath fieldPath, Object payload) { final AtomicReference hasField = new AtomicReference(false); traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { @@ -44,7 +44,7 @@ public void foundMatch(Match match) { return hasField.get(); } - Object extract(FieldPath path, Object payload) { + Object extract(JsonFieldPath path, Object payload) { final List matches = new ArrayList(); traverse(new ProcessingContext(payload, path), new MatchCallback() { @@ -65,7 +65,7 @@ public void foundMatch(Match match) { } } - void remove(final FieldPath path, Object payload) { + void remove(final JsonFieldPath path, Object payload) { traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override @@ -78,7 +78,7 @@ public void foundMatch(Match match) { private void traverse(ProcessingContext context, MatchCallback matchCallback) { final String segment = context.getSegment(); - if (FieldPath.isArraySegment(segment)) { + if (JsonFieldPath.isArraySegment(segment)) { if (context.getPayload() instanceof List) { handleListPayload(context, matchCallback); } @@ -206,13 +206,13 @@ private static final class ProcessingContext { private final Match parent; - private final FieldPath path; + private final JsonFieldPath path; - private ProcessingContext(Object payload, FieldPath path) { + private ProcessingContext(Object payload, JsonFieldPath path) { this(payload, path, null, null); } - private ProcessingContext(Object payload, FieldPath path, List segments, + private ProcessingContext(Object payload, JsonFieldPath path, List segments, Match parent) { this.payload = payload; this.path = path; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java similarity index 97% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java index 7eca8684d..0e3b287ad 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java @@ -25,7 +25,7 @@ * * @author Andy Wilkinson */ -public enum FieldType { +public enum JsonFieldType { ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java similarity index 66% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java index dff50cdae..ca55dac54 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java @@ -20,26 +20,26 @@ import java.util.Map; /** - * Resolves the type of a field in a request or response payload + * Resolves the type of a field in a JSON request or response payload * * @author Andy Wilkinson */ -class FieldTypeResolver { +class JsonFieldTypeResolver { - private final FieldProcessor fieldProcessor = new FieldProcessor(); + private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - FieldType resolveFieldType(String path, Object payload) { - FieldPath fieldPath = FieldPath.compile(path); + JsonFieldType resolveFieldType(String path, Object payload) { + JsonFieldPath fieldPath = JsonFieldPath.compile(path); Object field = this.fieldProcessor.extract(fieldPath, payload); if (field instanceof Collection && !fieldPath.isPrecise()) { - FieldType commonType = null; + JsonFieldType commonType = null; for (Object item : (Collection) field) { - FieldType fieldType = determineFieldType(item); + JsonFieldType fieldType = determineFieldType(item); if (commonType == null) { commonType = fieldType; } else if (fieldType != commonType) { - return FieldType.VARIES; + return JsonFieldType.VARIES; } } return commonType; @@ -47,22 +47,22 @@ else if (fieldType != commonType) { return determineFieldType(this.fieldProcessor.extract(fieldPath, payload)); } - private FieldType determineFieldType(Object fieldValue) { + private JsonFieldType determineFieldType(Object fieldValue) { if (fieldValue == null) { - return FieldType.NULL; + return JsonFieldType.NULL; } if (fieldValue instanceof String) { - return FieldType.STRING; + return JsonFieldType.STRING; } if (fieldValue instanceof Map) { - return FieldType.OBJECT; + return JsonFieldType.OBJECT; } if (fieldValue instanceof Collection) { - return FieldType.ARRAY; + return JsonFieldType.ARRAY; } if (fieldValue instanceof Boolean) { - return FieldType.BOOLEAN; + return JsonFieldType.BOOLEAN; } - return FieldType.NUMBER; + return JsonFieldType.NUMBER; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java new file mode 100644 index 000000000..93c824cbf --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java @@ -0,0 +1,92 @@ +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * A {@link PayloadHandler} for JSON payloads + * + * @author Andy Wilkinson + */ +class JsonPayloadHandler implements PayloadHandler { + + private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); + + private final ObjectMapper objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + private final String rawPayload; + + JsonPayloadHandler(String payload) throws IOException { + this.rawPayload = payload; + } + + @Override + public List findMissingFields(List fieldDescriptors) { + List missingFields = new ArrayList(); + Object payload = readPayload(); + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + if (!fieldDescriptor.isOptional() + && !this.fieldProcessor.hasField( + JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { + missingFields.add(fieldDescriptor); + } + } + + return missingFields; + } + + @Override + public String getUndocumentedPayload(List fieldDescriptors) { + Object payload = readPayload(); + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); + this.fieldProcessor.remove(path, payload); + } + if (!isEmpty(payload)) { + try { + return this.objectMapper.writeValueAsString(payload); + } + catch (JsonProcessingException ex) { + throw new PayloadHandlingException(ex); + } + } + return null; + } + + private Object readPayload() { + try { + return new ObjectMapper().readValue(this.rawPayload, Object.class); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + private boolean isEmpty(Object object) { + if (object instanceof Map) { + return ((Map) object).isEmpty(); + } + return ((List) object).isEmpty(); + } + + @Override + public Object determineFieldType(String path) { + try { + return new JsonFieldTypeResolver().resolveFieldType(path, readPayload()); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + path + "' as" + + " it is not present in the payload. Please provide a type using" + + " FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 40e047046..72b0494a5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -37,8 +37,12 @@ private PayloadDocumentation() { * Creates a {@code FieldDescriptor} that describes a field with the given * {@code path}. *

- * The {@code path} uses '.' to descend into a child object and ' {@code []}' to - * descend into an array. For example, with this JSON payload: + * When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to + * descend to a child node. + *

+ * When documenting a JSON payload, the {@code path} uses '.' to descend into a child + * object and ' {@code []}' to descend into an array. For example, with this JSON + * payload: * *

 	 * {
@@ -132,8 +136,7 @@ public static Snippet requestFields(FieldDescriptor... descriptors) {
 	 */
 	public static Snippet requestFields(Map attributes,
 			FieldDescriptor... descriptors) {
-		return new RequestFieldsSnippet(attributes,
-				Arrays.asList(descriptors));
+		return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors));
 	}
 
 	/**
@@ -174,8 +177,7 @@ public static Snippet responseFields(FieldDescriptor... descriptors) {
 	 */
 	public static Snippet responseFields(Map attributes,
 			FieldDescriptor... descriptors) {
-		return new ResponseFieldsSnippet(attributes,
-				Arrays.asList(descriptors));
+		return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors));
 	}
 
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java
new file mode 100644
index 000000000..5d6c2d479
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java
@@ -0,0 +1,45 @@
+package org.springframework.restdocs.payload;
+
+import java.util.List;
+
+/**
+ * A handler for a request or response payload
+ * 
+ * @author Andy Wilkinson
+ */
+interface PayloadHandler {
+
+	/**
+	 * Finds the fields that are missing from the handler's payload. A field is missing if
+	 * it is described by one of the {@code fieldDescriptors} but is not present in the
+	 * payload.
+	 * 
+	 * @param fieldDescriptors the descriptors
+	 * @return descriptors for the fields that are missing from the payload
+	 * @throws PayloadHandlingException if a failure occurs
+	 */
+	List findMissingFields(List fieldDescriptors);
+
+	/**
+	 * Returns a modified payload, formatted as a String, that only contains the fields
+	 * that are undocumented. A field is undocumented if it is present in the handler's
+	 * payload but is not described by the given {@code fieldDescriptors}. If the payload
+	 * is completely documented, {@code null} is returned
+	 * 
+	 * @param fieldDescriptors the descriptors
+	 * @return the undocumented payload, or {@code null} if all of the payload is
+	 * documented
+	 * @throws PayloadHandlingException if a failure occurs
+	 */
+	String getUndocumentedPayload(List fieldDescriptors);
+
+	/**
+	 * Returns the type of the field with the given {@code path} based on the content of
+	 * the payload.
+	 * 
+	 * @param path the field path
+	 * @return the type of the field
+	 */
+	Object determineFieldType(String path);
+
+}
\ No newline at end of file
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java
new file mode 100644
index 000000000..a569b39b9
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java
@@ -0,0 +1,20 @@
+package org.springframework.restdocs.payload;
+
+/**
+ * Thrown to indicate that a failure has occurred during payload handling
+ * 
+ * @author Andy Wilkinson
+ *
+ */
+@SuppressWarnings("serial")
+class PayloadHandlingException extends RuntimeException {
+
+	/**
+	 * Creates a new {@code PayloadHandlingException} with the given cause
+	 * @param cause the cause of the failure
+	 */
+	PayloadHandlingException(Throwable cause) {
+		super(cause);
+	}
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
index bff0832fd..c7e25c9b2 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java
@@ -20,6 +20,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.springframework.http.MediaType;
 import org.springframework.restdocs.snippet.Snippet;
 import org.springframework.test.web.servlet.MvcResult;
 
@@ -43,4 +44,13 @@ protected Reader getPayloadReader(MvcResult result) throws IOException {
 		return result.getRequest().getReader();
 	}
 
+	@Override
+	protected MediaType getContentType(MvcResult result) {
+		String contentType = result.getRequest().getContentType();
+		if (contentType != null) {
+			return MediaType.valueOf(contentType);
+		}
+		return null;
+	}
+
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
index 2972d80df..440e5b719 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java
@@ -21,11 +21,12 @@
 import java.util.List;
 import java.util.Map;
 
+import org.springframework.http.MediaType;
 import org.springframework.restdocs.snippet.Snippet;
 import org.springframework.test.web.servlet.MvcResult;
 
 /**
- * A {@link Snippet} the documents the fields in a response.
+ * A {@link Snippet} that documents the fields in a response.
  * 
  * @author Andy Wilkinson
  */
@@ -45,4 +46,13 @@ protected Reader getPayloadReader(MvcResult result) throws IOException {
 		return new StringReader(result.getResponse().getContentAsString());
 	}
 
+	@Override
+	protected MediaType getContentType(MvcResult result) {
+		String contentType = result.getResponse().getContentType();
+		if (contentType != null) {
+			return MediaType.valueOf(contentType);
+		}
+		return null;
+	}
+
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java
new file mode 100644
index 000000000..c50aaba03
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java
@@ -0,0 +1,141 @@
+package org.springframework.restdocs.payload;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * A {@link PayloadHandler} for XML payloads
+ * 
+ * @author Andy Wilkinson
+ */
+class XmlPayloadHandler implements PayloadHandler {
+
+	private final DocumentBuilder documentBuilder;
+
+	private final String rawPayload;
+
+	XmlPayloadHandler(String rawPayload) {
+		try {
+			this.documentBuilder = DocumentBuilderFactory.newInstance()
+					.newDocumentBuilder();
+		}
+		catch (ParserConfigurationException ex) {
+			throw new IllegalStateException("Failed to create document builder", ex);
+		}
+		this.rawPayload = rawPayload;
+	}
+
+	@Override
+	public List findMissingFields(List fieldDescriptors) {
+		List missingFields = new ArrayList<>();
+		Document payload = readPayload();
+		for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
+			if (!fieldDescriptor.isOptional()) {
+				NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload);
+				if (matchingNodes.getLength() == 0) {
+					missingFields.add(fieldDescriptor);
+				}
+			}
+		}
+
+		return missingFields;
+	}
+
+	private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) {
+		try {
+			return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload,
+					XPathConstants.NODESET);
+		}
+		catch (XPathExpressionException ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	private Document readPayload() {
+		try {
+			return this.documentBuilder.parse(new InputSource(new StringReader(
+					this.rawPayload)));
+		}
+		catch (Exception ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	private XPathExpression createXPath(String fieldPath) throws XPathExpressionException {
+		return XPathFactory.newInstance().newXPath().compile(fieldPath);
+	}
+
+	@Override
+	public String getUndocumentedPayload(List fieldDescriptors) {
+		Document payload = readPayload();
+		for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
+			NodeList matchingNodes;
+			try {
+				matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath())
+						.evaluate(payload, XPathConstants.NODESET);
+			}
+			catch (XPathExpressionException ex) {
+				throw new PayloadHandlingException(ex);
+			}
+			for (int i = 0; i < matchingNodes.getLength(); i++) {
+				Node node = matchingNodes.item(i);
+				node.getParentNode().removeChild(node);
+			}
+		}
+		if (payload.getChildNodes().getLength() > 0) {
+			return prettyPrint(payload);
+		}
+		return null;
+	}
+
+	private String prettyPrint(Document document) {
+		try {
+			StringWriter stringWriter = new StringWriter();
+			StreamResult xmlOutput = new StreamResult(stringWriter);
+			TransformerFactory transformerFactory = TransformerFactory.newInstance();
+			transformerFactory.setAttribute("indent-number", 4);
+			Transformer transformer = transformerFactory.newTransformer();
+			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+			transformer.transform(new DOMSource(document), xmlOutput);
+			return xmlOutput.getWriter().toString();
+		}
+		catch (Exception ex) {
+			throw new PayloadHandlingException(ex);
+		}
+	}
+
+	@Override
+	public Object determineFieldType(String path) {
+		try {
+			return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
+		}
+		catch (FieldDoesNotExistException ex) {
+			String message = "Cannot determine the type of the field '" + path + "' as"
+					+ " it is not present in the payload. Please provide a type using"
+					+ " FieldDescriptor.type(Object type).";
+			throw new FieldTypeRequiredException(message);
+		}
+	}
+
+}
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
deleted file mode 100644
index 53f118f4c..000000000
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java
+++ /dev/null
@@ -1,114 +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.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.snippet.SnippetException;
-
-/**
- * Tests for {@link FieldValidator}
- * 
- * @author Andy Wilkinson
- */
-public class FieldValidatorTests {
-
-	private final FieldValidator fieldValidator = new FieldValidator();
-
-	@Rule
-	public ExpectedException thrownException = ExpectedException.none();
-
-	private StringReader listPayload = new StringReader(
-			"[{\"a\":1},{\"a\":2},{\"b\":{\"c\":3}}]");
-
-	private StringReader payload = new StringReader(
-			"{\"a\":{\"b\":{},\"c\":true,\"d\":[{\"e\":1},{\"e\":2}]}}");
-
-	@Test
-	public void noMissingFieldsAllFieldsDocumented() throws IOException {
-		this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
-				"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"),
-				new FieldDescriptor("a.d"), new FieldDescriptor("a")));
-	}
-
-	@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("a.d"),
-				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"), new FieldDescriptor("a.d[].e")));
-	}
-
-	@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(SnippetException.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(SnippetException.class);
-		this.thrownException.expectMessage(equalTo(String
-				.format("The following parts 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"), new FieldDescriptor("a.d")));
-	}
-
-	@Test
-	public void listPayloadNoMissingFieldsAllFieldsDocumented() throws IOException {
-		this.fieldValidator.validate(this.listPayload, Arrays.asList(new FieldDescriptor(
-				"[]b.c"), new FieldDescriptor("[]b"), new FieldDescriptor("[]a"),
-				new FieldDescriptor("[]")));
-	}
-
-	@Test
-	public void listPayloadParentIsDocumentedWhenAllChildrenAreDocumented()
-			throws IOException {
-		this.fieldValidator.validate(this.listPayload,
-				Arrays.asList(new FieldDescriptor("[]b.c"), new FieldDescriptor("[]a")));
-	}
-
-	@Test
-	public void listPayloadChildIsDocumentedWhenParentIsDocumented() throws IOException {
-		this.fieldValidator.validate(this.listPayload,
-				Arrays.asList(new FieldDescriptor("[]")));
-	}
-}
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
similarity index 67%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
index 0e1857201..f94beee49 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldPathTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java
@@ -24,89 +24,89 @@
 import org.junit.Test;
 
 /**
- * Tests for {@link FieldPath}
+ * Tests for {@link JsonFieldPath}
  * 
  * @author Andy Wilkinson
  */
-public class FieldPathTests {
+public class JsonFieldPathTests {
 
 	@Test
 	public void singleFieldIsPrecise() {
-		assertTrue(FieldPath.compile("a").isPrecise());
+		assertTrue(JsonFieldPath.compile("a").isPrecise());
 	}
 
 	@Test
 	public void singleNestedFieldIsPrecise() {
-		assertTrue(FieldPath.compile("a.b").isPrecise());
+		assertTrue(JsonFieldPath.compile("a.b").isPrecise());
 	}
 
 	@Test
 	public void topLevelArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("[]").isPrecise());
 	}
 
 	@Test
 	public void fieldBeneathTopLevelArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("[]a").isPrecise());
+		assertFalse(JsonFieldPath.compile("[]a").isPrecise());
 	}
 
 	@Test
 	public void arrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[]").isPrecise());
 	}
 
 	@Test
 	public void nestedArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a.b[]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a.b[]").isPrecise());
 	}
 
 	@Test
 	public void arrayOfArraysIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[][]").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[][]").isPrecise());
 	}
 
 	@Test
 	public void fieldBeneathAnArrayIsNotPrecise() {
-		assertFalse(FieldPath.compile("a[].b").isPrecise());
+		assertFalse(JsonFieldPath.compile("a[].b").isPrecise());
 	}
 
 	@Test
 	public void compilationOfSingleElementPath() {
-		assertThat(FieldPath.compile("a").getSegments(), contains("a"));
+		assertThat(JsonFieldPath.compile("a").getSegments(), contains("a"));
 	}
 
 	@Test
 	public void compilationOfMultipleElementPath() {
-		assertThat(FieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
+		assertThat(JsonFieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithNoDotSeparators() {
-		assertThat(FieldPath.compile("a[]b[]c").getSegments(),
+		assertThat(JsonFieldPath.compile("a[]b[]c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPreAndPostDotSeparators() {
-		assertThat(FieldPath.compile("a.[].b.[].c").getSegments(),
+		assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPreDotSeparators() {
-		assertThat(FieldPath.compile("a.[]b.[]c").getSegments(),
+		assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathWithArraysWithPostDotSeparators() {
-		assertThat(FieldPath.compile("a[].b[].c").getSegments(),
+		assertThat(JsonFieldPath.compile("a[].b[].c").getSegments(),
 				contains("a", "[]", "b", "[]", "c"));
 	}
 
 	@Test
 	public void compilationOfPathStartingWithAnArray() {
-		assertThat(FieldPath.compile("[]a.b.c").getSegments(),
+		assertThat(JsonFieldPath.compile("[]a.b.c").getSegments(),
 				contains("[]", "a", "b", "c"));
 	}
 
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
similarity index 80%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
index da71af5e0..bc61a3388 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldProcessorTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java
@@ -30,19 +30,19 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 /**
- * Tests for {@link FieldProcessor}
+ * Tests for {@link JsonFieldProcessor}
  * 
  * @author Andy Wilkinson
  */
-public class FieldProcessorTests {
+public class JsonFieldProcessorTests {
 
-	private final FieldProcessor fieldProcessor = new FieldProcessor();
+	private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
 
 	@Test
 	public void extractTopLevelMapEntry() {
 		Map payload = new HashMap<>();
 		payload.put("a", "alpha");
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
 				equalTo((Object) "alpha"));
 	}
 
@@ -52,7 +52,7 @@ public void extractNestedMapEntry() {
 		Map alpha = new HashMap<>();
 		payload.put("a", alpha);
 		alpha.put("b", "bravo");
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a.b"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload),
 				equalTo((Object) "bravo"));
 	}
 
@@ -63,7 +63,7 @@ public void extractArray() {
 		bravo.put("b", "bravo");
 		List> alpha = Arrays.asList(bravo, bravo);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
 				equalTo((Object) alpha));
 	}
 
@@ -74,7 +74,7 @@ public void extractArrayContents() {
 		bravo.put("b", "bravo");
 		List> alpha = Arrays.asList(bravo, bravo);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[]"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload),
 				equalTo((Object) alpha));
 	}
 
@@ -85,7 +85,7 @@ public void extractFromItemsInArray() {
 		entry.put("b", "bravo");
 		List> alpha = Arrays.asList(entry, entry);
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[].b"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[].b"), payload),
 				equalTo((Object) Arrays.asList("bravo", "bravo")));
 	}
 
@@ -98,7 +98,7 @@ public void extractNestedArray() {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][]"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload),
 				equalTo((Object) Arrays.asList(entry1, entry2, entry3)));
 	}
 
@@ -111,7 +111,7 @@ public void extractFromItemsInNestedArray() {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].id"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload),
 				equalTo((Object) Arrays.asList("1", "2", "3")));
 	}
 
@@ -124,7 +124,7 @@ public void extractArraysFromItemsInNestedArray() {
 		List>> alpha = Arrays.asList(
 				Arrays.asList(entry1, entry2), Arrays.asList(entry3));
 		payload.put("a", alpha);
-		assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].ids"), payload),
+		assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload),
 				equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3),
 						Arrays.asList(4))));
 	}
@@ -132,21 +132,21 @@ public void extractArraysFromItemsInNestedArray() {
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentTopLevelField() {
 		this.fieldProcessor
-				.extract(FieldPath.compile("a"), new HashMap());
+				.extract(JsonFieldPath.compile("a"), new HashMap());
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentNestedField() {
 		HashMap payload = new HashMap();
 		payload.put("a", new HashMap());
-		this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentNestedFieldWhenParentIsNotAMap() {
 		HashMap payload = new HashMap();
 		payload.put("a", 5);
-		this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
@@ -155,20 +155,20 @@ public void nonExistentFieldWhenParentIsAnArray() {
 		HashMap alpha = new HashMap();
 		alpha.put("b", Arrays.asList(new HashMap()));
 		payload.put("a", alpha);
-		this.fieldProcessor.extract(FieldPath.compile("a.b.c"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentArrayField() {
 		HashMap payload = new HashMap();
-		this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
 	public void nonExistentArrayFieldAsTypeDoesNotMatch() {
 		HashMap payload = new HashMap();
 		payload.put("a", 5);
-		this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
 	}
 
 	@Test(expected = FieldDoesNotExistException.class)
@@ -177,14 +177,14 @@ public void nonExistentFieldBeneathAnArray() {
 		HashMap alpha = new HashMap();
 		alpha.put("b", Arrays.asList(new HashMap()));
 		payload.put("a", alpha);
-		this.fieldProcessor.extract(FieldPath.compile("a.b[].id"), payload);
+		this.fieldProcessor.extract(JsonFieldPath.compile("a.b[].id"), payload);
 	}
 
 	@Test
 	public void removeTopLevelMapEntry() {
 		Map payload = new HashMap<>();
 		payload.put("a", "alpha");
-		this.fieldProcessor.remove(FieldPath.compile("a"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -194,7 +194,7 @@ public void removeNestedMapEntry() {
 		Map alpha = new HashMap<>();
 		payload.put("a", alpha);
 		alpha.put("b", "bravo");
-		this.fieldProcessor.remove(FieldPath.compile("a.b"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a.b"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -203,7 +203,7 @@ public void removeNestedMapEntry() {
 	public void removeItemsInArray() throws IOException {
 		Map payload = new ObjectMapper().readValue(
 				"{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class);
-		this.fieldProcessor.remove(FieldPath.compile("a[].b"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
@@ -212,7 +212,7 @@ public void removeItemsInArray() throws IOException {
 	public void removeItemsInNestedArray() throws IOException {
 		Map payload = new ObjectMapper().readValue(
 				"{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class);
-		this.fieldProcessor.remove(FieldPath.compile("a[][].id"), payload);
+		this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload);
 		assertThat(payload.size(), equalTo(0));
 	}
 
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
similarity index 79%
rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java
rename to spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
index 4376bea55..468689512 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java
@@ -29,66 +29,66 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 /**
- * Tests for {@link FieldTypeResolver}
+ * Tests for {@link JsonFieldTypeResolver}
  * 
  * @author Andy Wilkinson
  *
  */
-public class FieldTypeResolverTests {
+public class JsonFieldTypeResolverTests {
 
-	private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
+	private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver();
 
 	@Rule
 	public ExpectedException thrownException = ExpectedException.none();
 
 	@Test
 	public void arrayField() throws IOException {
-		assertFieldType(FieldType.ARRAY, "[]");
+		assertFieldType(JsonFieldType.ARRAY, "[]");
 	}
 
 	@Test
 	public void booleanField() throws IOException {
-		assertFieldType(FieldType.BOOLEAN, "true");
+		assertFieldType(JsonFieldType.BOOLEAN, "true");
 	}
 
 	@Test
 	public void objectField() throws IOException {
-		assertFieldType(FieldType.OBJECT, "{}");
+		assertFieldType(JsonFieldType.OBJECT, "{}");
 	}
 
 	@Test
 	public void nullField() throws IOException {
-		assertFieldType(FieldType.NULL, "null");
+		assertFieldType(JsonFieldType.NULL, "null");
 	}
 
 	@Test
 	public void numberField() throws IOException {
-		assertFieldType(FieldType.NUMBER, "1.2345");
+		assertFieldType(JsonFieldType.NUMBER, "1.2345");
 	}
 
 	@Test
 	public void stringField() throws IOException {
-		assertFieldType(FieldType.STRING, "\"Foo\"");
+		assertFieldType(JsonFieldType.STRING, "\"Foo\"");
 	}
 
 	@Test
 	public void nestedField() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c",
-				createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT));
+				createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT));
 	}
 
 	@Test
 	public void multipleFieldsWithSameType() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
 				createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")),
-				equalTo(FieldType.NUMBER));
+				equalTo(JsonFieldType.NUMBER));
 	}
 
 	@Test
 	public void multipleFieldsWithDifferentTypes() throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
 				createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")),
-				equalTo(FieldType.VARIES));
+				equalTo(JsonFieldType.VARIES));
 	}
 
 	@Test
@@ -99,7 +99,7 @@ public void nonExistentFieldProducesIllegalArgumentException() throws IOExceptio
 		this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}"));
 	}
 
-	private void assertFieldType(FieldType expectedType, String jsonValue)
+	private void assertFieldType(JsonFieldType expectedType, String jsonValue)
 			throws IOException {
 		assertThat(this.fieldTypeResolver.resolveFieldType("field",
 				createSimplePayload(jsonValue)), equalTo(expectedType));
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
index d81ce320a..fb257e54e 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java
@@ -36,6 +36,7 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.restdocs.snippet.SnippetException;
@@ -65,8 +66,8 @@ public void mapRequestWithFields() throws IOException {
 						.row("a.c", "String", "two") //
 						.row("a", "Object", "three"));
 
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"), fieldWithPath("a.c").description("two"),
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"),
+				fieldWithPath("a.c").description("two"),
 				fieldWithPath("a").description("three"))).document(
 				"map-request-with-fields",
 				result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
@@ -80,9 +81,9 @@ public void arrayRequestWithFields() throws IOException {
 						.row("[]a.c", "String", "two") //
 						.row("[]a", "Object", "three"));
 
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
-				.description("one"), fieldWithPath("[]a.c").description("two"),
-				fieldWithPath("[]a").description("three"))).document(
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"),
+				fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a")
+						.description("three"))).document(
 				"array-request-with-fields",
 				result(get("/foo").content(
 						"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
@@ -94,9 +95,8 @@ public void undocumentedRequestField() throws IOException {
 		this.thrown
 				.expectMessage(startsWith("The following parts of the payload were not"
 						+ " documented:"));
-		new RequestFieldsSnippet(Collections. emptyList())
-				.document("undocumented-request-field",
-						result(get("/foo").content("{\"a\": 5}")));
+		new RequestFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-request-field", result(get("/foo").content("{\"a\": 5}")));
 	}
 
 	@Test
@@ -105,18 +105,16 @@ public void missingRequestField() throws IOException {
 		this.thrown
 				.expectMessage(equalTo("Fields with the following paths were not found"
 						+ " in the payload: [a.b]"));
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"))).document("missing-request-fields",
-				result(get("/foo").content("{}")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
+				.document("missing-request-fields", result(get("/foo").content("{}")));
 	}
 
 	@Test
 	public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
 		this.thrown.expect(FieldTypeRequiredException.class);
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one").optional())).document(
-				"missing-optional-request-field-with-no-type", result(get("/foo")
-						.content("{ }")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")
+				.optional())).document("missing-optional-request-field-with-no-type",
+				result(get("/foo").content("{ }")));
 	}
 
 	@Test
@@ -128,10 +126,9 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException
 		this.thrown
 				.expectMessage(endsWith("Fields with the following paths were not found"
 						+ " in the payload: [a.b]"));
-		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
-				.description("one"))).document(
-				"undocumented-request-field-and-missing-request-field",
-				result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
+				.document("undocumented-request-field-and-missing-request-field",
+						result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
 	}
 
 	@Test
@@ -171,10 +168,74 @@ public void requestFieldsWithCustomAttributes() throws IOException {
 				resolver));
 		request.setContent("{\"a\": \"foo\"}".getBytes());
 		MockHttpServletResponse response = new MockHttpServletResponse();
-		new RequestFieldsSnippet(attributes(key("title").value(
-				"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
-				.document("request-fields-with-custom-attributes",
-						result(request, response));
+		new RequestFieldsSnippet(attributes(key("title").value("Custom title")),
+				Arrays.asList(fieldWithPath("a").description("one"))).document(
+				"request-fields-with-custom-attributes", result(request, response));
+	}
+
+	@Test
+	public void xmlRequestFields() throws IOException {
+		this.snippet.expectRequestFields("xml-request").withContents( //
+				tableWithHeader("Path", "Type", "Description") //
+						.row("a/b", "b", "one") //
+						.row("a/c", "c", "two") //
+						.row("a", "a", "three"));
+
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
+				.type("b"), fieldWithPath("a/c").description("two").type("c"),
+				fieldWithPath("a").description("three").type("a"))).document(
+				"xml-request",
+				result(get("/foo").content("5charlie").contentType(
+						MediaType.APPLICATION_XML)));
+	}
+
+	@Test
+	public void undocumentedXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		new RequestFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-xml-request-field",
+				result(get("/foo").content("5").contentType(
+						MediaType.APPLICATION_XML)));
+	}
+
+	@Test
+	public void xmlRequestFieldWithNoType() throws IOException {
+		this.thrown.expect(FieldTypeRequiredException.class);
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
+				.document("missing-xml-request",
+						result(get("/foo").contentType(MediaType.APPLICATION_XML)
+								.content("5")));
+	}
+
+	@Test
+	public void missingXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(equalTo("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
+				fieldWithPath("a").description("one"))).document(
+				"missing-xml-request-fields",
+				result(get("/foo").contentType(MediaType.APPLICATION_XML).content(
+						"")));
+	}
+
+	@Test
+	public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		this.thrown
+				.expectMessage(endsWith("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
+				.document("undocumented-xml-request-field-and-missing-xml-request-field",
+						result(get("/foo").contentType(MediaType.APPLICATION_XML)
+								.content("5")));
 	}
 
 	private FileSystemResource snippetResource(String name) {
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
index b39802b53..a768f05b0 100644
--- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java
@@ -15,6 +15,8 @@
  */
 package org.springframework.restdocs.payload;
 
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.startsWith;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -26,13 +28,16 @@
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.restdocs.snippet.SnippetException;
 import org.springframework.restdocs.templates.TemplateEngine;
 import org.springframework.restdocs.templates.TemplateResourceResolver;
 import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
@@ -66,10 +71,10 @@ public void mapResponseWithFields() throws IOException {
 		response.getWriter().append(
 				"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
 						+ " [{\"id\":356,\"name\": \"sample\"}]}");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id")
-				.description("one"), fieldWithPath("date").description("two"),
-				fieldWithPath("assets").description("three"), fieldWithPath("assets[]")
-						.description("four"),
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"),
+				fieldWithPath("date").description("two"), fieldWithPath("assets")
+						.description("three"),
+				fieldWithPath("assets[]").description("four"),
 				fieldWithPath("assets[].id").description("five"),
 				fieldWithPath("assets[].name").description("six"))).document(
 				"map-response-with-fields", result(response));
@@ -86,10 +91,10 @@ public void arrayResponseWithFields() throws IOException {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getWriter()
 				.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
-				.description("one"), fieldWithPath("[]a.c").description("two"),
-				fieldWithPath("[]a").description("three"))).document(
-				"array-response-with-fields", result(response));
+		new ResponseFieldsSnippet(Arrays.asList(
+				fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c")
+						.description("two"), fieldWithPath("[]a").description("three")))
+				.document("array-response-with-fields", result(response));
 	}
 
 	@Test
@@ -100,8 +105,8 @@ public void arrayResponse() throws IOException {
 
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getWriter().append("[\"a\", \"b\", \"c\"]");
-		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]")
-				.description("one"))).document("array-response", result(response));
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one")))
+				.document("array-response", result(response));
 	}
 
 	@Test
@@ -142,10 +147,80 @@ public void responseFieldsWithCustomAttributes() throws IOException {
 				resolver));
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		response.getOutputStream().print("{\"a\": \"foo\"}");
-		new ResponseFieldsSnippet(attributes(key("title").value(
-				"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
-				.document("response-fields-with-custom-attributes",
-						result(request, response));
+		new ResponseFieldsSnippet(attributes(key("title").value("Custom title")),
+				Arrays.asList(fieldWithPath("a").description("one"))).document(
+				"response-fields-with-custom-attributes", result(request, response));
+	}
+
+	@Test
+	public void xmlResponseFields() throws IOException {
+		this.snippet.expectResponseFields("xml-response").withContents( //
+				tableWithHeader("Path", "Type", "Description") //
+						.row("a/b", "b", "one") //
+						.row("a/c", "c", "two") //
+						.row("a", "a", "three"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5charlie");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
+				.type("b"), fieldWithPath("a/c").description("two").type("c"),
+				fieldWithPath("a").description("three").type("a"))).document(
+				"xml-response", result(response));
+	}
+
+	@Test
+	public void undocumentedXmlResponseField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Collections. emptyList()).document(
+				"undocumented-xml-response-field", result(response));
+	}
+
+	@Test
+	public void xmlResponseFieldWithNoType() throws IOException {
+		this.thrown.expect(FieldTypeRequiredException.class);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
+				.document("xml-response-no-field-type", result(response));
+	}
+
+	@Test
+	public void missingXmlResponseField() throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(equalTo("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
+				fieldWithPath("a").description("one"))).document(
+				"missing-xml-response-field", result(response));
+	}
+
+	@Test
+	public void undocumentedXmlResponseFieldAndMissingXmlResponseField()
+			throws IOException {
+		this.thrown.expect(SnippetException.class);
+		this.thrown
+				.expectMessage(startsWith("The following parts of the payload were not"
+						+ " documented:"));
+		this.thrown
+				.expectMessage(endsWith("Fields with the following paths were not found"
+						+ " in the payload: [a/b]"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		response.setContentType(MediaType.APPLICATION_XML_VALUE);
+		response.getOutputStream().print("5");
+		new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
+				.document("undocumented-xml-request-field-and-missing-xml-request-field",
+						result(response));
 	}
 
 	private FileSystemResource snippetResource(String name) {

From 4199a21bfd4c5c3f91d77706b31f9a00904f3a1a Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 19 Aug 2015 17:15:36 +0100
Subject: [PATCH 0112/1059] Documentation polishing

---
 docs/src/docs/asciidoc/documenting-your-api.adoc | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc
index 02a5d6976..d7fa3f753 100644
--- a/docs/src/docs/asciidoc/documenting-your-api.adoc
+++ b/docs/src/docs/asciidoc/documenting-your-api.adoc
@@ -65,8 +65,9 @@ provided:
 ----
 include::{examples-dir}/com/example/Payload.java[tags=response]
 ----
-<1> Use `withResponseFields` to describe the expected fields in the response payload.
-To document a request `withRequestFields` can be used.
+<1> Use `responseFields` to describe the expected fields in the response payload.
+To document a request `requestFields` can be used. Both are static methods on
+`org.springframework.restdocs.payload.PayloadDocumentation`.
 <2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on
 `org.springframework.restdocs.payload.PayloadDocumentation`.
 <3> Expect a field with the path `contact.email`.
@@ -355,9 +356,8 @@ There are two ways to provide extra information for inclusion in a generated sni
 
 . Use the `attributes` method on a field, link or parameter descriptor to add one or more
   attributes to an individual descriptor.
-. Pass in some attributes when calling `withCurlRequest`, `withHttpRequest`,
-  `withHttpResponse`, etc on `RestDocumentationResultHandler`. Such attributes will be
-  associated with the snippet as a whole.
+. Pass in some attributes when calling `curlRequest`, `httpRequest`, `httpResponse`, etc.
+  Such attributes will be associated with the snippet as a whole.
 
 Any additional attributes are made available during the template rendering process.
 Coupled with a custom snippet template, this makes it possible to include extra

From 32bb724a3dcdffb57ee8b7219a273d74649a24a9 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 19 Aug 2015 17:25:56 +0100
Subject: [PATCH 0113/1059] Update the samples to align with recent API changes

---
 .../src/test/java/com/example/notes/ApiDocumentation.java   | 6 +++---
 .../src/test/java/com/example/notes/ApiDocumentation.java   | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

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 f58d92adf..cfa00bde1 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
@@ -44,7 +44,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.SpringApplicationConfiguration;
 import org.springframework.hateoas.MediaTypes;
-import org.springframework.restdocs.payload.FieldType;
+import org.springframework.restdocs.payload.JsonFieldType;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.web.servlet.MockMvc;
@@ -279,8 +279,8 @@ public void noteUpdateExample() throws Exception {
 				.andExpect(status().isNoContent())
 				.andDo(document("note-update-example",
 						requestFields(
-								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("title").description("The title of the note").type(JsonFieldType.STRING).optional(),
+								fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(),
 								fieldWithPath("tags").description("An array of tag resource URIs").optional())));
 	}
 
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 c8c7dca22..1bf108109 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
@@ -44,7 +44,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.SpringApplicationConfiguration;
 import org.springframework.hateoas.MediaTypes;
-import org.springframework.restdocs.payload.FieldType;
+import org.springframework.restdocs.payload.JsonFieldType;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.web.servlet.MockMvc;
@@ -267,8 +267,8 @@ public void noteUpdateExample() throws Exception {
 				.andExpect(status().isNoContent())
 				.andDo(document("note-update-example",
 						requestFields(
-								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("title").description("The title of the note").type(JsonFieldType.STRING).optional(),
+								fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(),
 								fieldWithPath("tags").description("An array of tag resource URIs").optional())));
 	}
 

From 2842d5f75f8f57935de75414bd3be3a103d92962 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 19 Aug 2015 17:43:31 +0100
Subject: [PATCH 0114/1059] Update Travis CI configuration to run on new
 infrastructure

---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.travis.yml b/.travis.yml
index 0477e7d0e..d84c55194 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,4 @@
+sudo: false
 language: java
 jdk:
   - oraclejdk7

From bc5a9c371485016720b8aee9ff0c2bdef646bb40 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 19 Aug 2015 18:01:25 +0100
Subject: [PATCH 0115/1059] Update the samples to use Spring Boot 1.2.5

---
 samples/rest-notes-spring-data-rest/build.gradle |  2 +-
 samples/rest-notes-spring-data-rest/pom.xml      |  2 +-
 samples/rest-notes-spring-hateoas/build.gradle   |  6 ++----
 samples/rest-notes-spring-hateoas/pom.xml        | 15 ++-------------
 4 files changed, 6 insertions(+), 19 deletions(-)

diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle
index 93d03fb04..b30c212be 100644
--- a/samples/rest-notes-spring-data-rest/build.gradle
+++ b/samples/rest-notes-spring-data-rest/build.gradle
@@ -3,7 +3,7 @@ buildscript {
 		mavenCentral()
 	}
 	dependencies {
-		classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE'
+		classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE'
 	}
 }
 
diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml
index 26e4f7e31..b553763a4 100644
--- a/samples/rest-notes-spring-data-rest/pom.xml
+++ b/samples/rest-notes-spring-data-rest/pom.xml
@@ -11,7 +11,7 @@
 	
 		org.springframework.boot
 		spring-boot-starter-parent
-		1.2.4.RELEASE
+		1.2.5.RELEASE
 		
 	
 
diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle
index d7c9a4c31..5a2e164db 100644
--- a/samples/rest-notes-spring-hateoas/build.gradle
+++ b/samples/rest-notes-spring-hateoas/build.gradle
@@ -3,7 +3,7 @@ buildscript {
 		mavenCentral()
 	}
 	dependencies {
-		classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.4.RELEASE'
+		classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE'
 	}
 }
 
@@ -27,12 +27,10 @@ sourceCompatibility = 1.7
 targetCompatibility = 1.7
 
 dependencies {
-	compile 'org.springframework.boot:spring-boot-starter-web'
 	compile 'org.springframework.boot:spring-boot-starter-data-jpa'
-	compile 'org.springframework.hateoas:spring-hateoas'
+	compile 'org.springframework.boot:spring-boot-starter-hateoas'
 
 	runtime 'com.h2database:h2'
-	runtime 'org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE'
 	runtime 'org.atteo:evo-inflector:1.2'
 
 	testCompile 'com.jayway.jsonpath:json-path'
diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml
index b46f0db1c..a9a1a3398 100644
--- a/samples/rest-notes-spring-hateoas/pom.xml
+++ b/samples/rest-notes-spring-hateoas/pom.xml
@@ -11,7 +11,7 @@
 	
 		org.springframework.boot
 		spring-boot-starter-parent
-		1.2.4.RELEASE
+		1.2.5.RELEASE
 		
 	
 
@@ -24,23 +24,12 @@
 	
 		
 			org.springframework.boot
-			spring-boot-starter-web
+			spring-boot-starter-hateoas
 		
 		
 			org.springframework.boot
 			spring-boot-starter-data-jpa
 		
-		
-			org.springframework.hateoas
-			spring-hateoas
-		
-
-		
-			org.springframework.plugin
-			spring-plugin-core
-			1.1.0.RELEASE
-			runtime
-		
 		
 			com.h2database
 			h2

From ff822bd88da29d7ba2157752c8fd8a241837b807 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Tue, 25 Aug 2015 11:21:28 +0100
Subject: [PATCH 0116/1059] =?UTF-8?q?Provide=20an=20API=20to=20ease=20docu?=
 =?UTF-8?q?menting=20a=20request=20field=E2=80=99s=20constraints?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously, users that wanted to document a request field’s constraints
had to roll their own solution. This commit introduces a new API that
makes it easier to document constraints. Support is provided for
discovering Bean Validation constraints and resolving descriptions for
them. The constraint descriptions can then be used as required. For
example, they can included in a field’s description or in an additional
constraints attribute that’s included in an additional table column via
the use of a custom snippet template.

Closes gh-42
---
 build.gradle                                  |  16 +-
 docs/build.gradle                             |   1 +
 .../docs/asciidoc/documenting-your-api.adoc   |  86 ++++++++
 .../test/java/com/example/Constraints.java    |  31 +++
 .../com/example/notes/AbstractNoteInput.java  |  51 -----
 .../java/com/example/notes/NoteInput.java     |  30 ++-
 .../com/example/notes/NotePatchInput.java     |  29 ++-
 .../com/example/notes/NullOrNotBlank.java     |  16 +-
 .../main/java/com/example/notes/TagInput.java |  12 +-
 .../java/com/example/notes/TagPatchInput.java |  14 +-
 .../com/example/notes/ApiDocumentation.java   |  58 +++++-
 .../example/notes/NullOrNotBlankTests.java    |  70 +++++++
 .../ConstraintDescriptions.properties         |   2 +
 .../restdocs/templates/request-fields.snippet |  11 +
 spring-restdocs/build.gradle                  |  18 +-
 .../restdocs/constraints/Constraint.java      |  62 ++++++
 .../ConstraintDescriptionResolver.java        |  29 +--
 .../constraints/ConstraintDescriptions.java   | 108 ++++++++++
 .../constraints/ConstraintResolver.java       |  38 ++++
 ...ceBundleConstraintDescriptionResolver.java | 145 +++++++++++++
 .../ValidatorConstraintResolver.java          |  82 ++++++++
 .../DefaultConstraintDescriptions.properties  |  13 ++
 .../RestDocumentationIntegrationTests.java    |   1 +
 .../ConstraintDescriptionsTests.java          | 182 +++++++++++++++++
 ...dleConstraintDescriptionResolverTests.java | 192 ++++++++++++++++++
 .../ValidatorConstraintResolverTests.java     |  97 +++++++++
 .../TestConstraintDescriptions.properties     |   1 +
 27 files changed, 1291 insertions(+), 104 deletions(-)
 create mode 100644 docs/src/test/java/com/example/Constraints.java
 delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java
 create mode 100644 samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java
 create mode 100644 samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties
 create mode 100644 samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet
 create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java
 rename samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java => spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java (56%)
 create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java
 create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java
 create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java
 create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java
 create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties
 create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java
 create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java
 create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java
 create mode 100644 spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties

diff --git a/build.gradle b/build.gradle
index f44023b25..ee1baef93 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,11 @@
 buildscript {
 	repositories {
 		jcenter()
+		maven { url 'https://repo.spring.io/plugins-release' }
 	}
 	dependencies {
 		classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE'
+		classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7'
 	}
 }
 
@@ -13,27 +15,27 @@ allprojects {
 
 apply plugin: 'samples'
 
-ext {
-	springVersion = '4.1.7.RELEASE'
-	jmustacheVersion = '1.10'
-}
-
 subprojects {
 	apply plugin: 'io.spring.dependency-management'
 	apply plugin: 'java'
 	apply plugin: 'eclipse'
+	apply plugin: 'propdeps'
+	apply plugin: 'propdeps-eclipse'
+	apply plugin: 'propdeps-maven'
 
 	dependencyManagement {
 		imports {
-			mavenBom "org.springframework:spring-framework-bom:$springVersion"
+			mavenBom 'org.springframework:spring-framework-bom:4.1.7.RELEASE'
 		}
 		dependencies {
 			dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6'
-			dependency "com.samskivert:jmustache:$jmustacheVersion"
+			dependency 'com.samskivert:jmustache:1.10'
 			dependency 'javax.servlet:javax.servlet-api:3.1.0'
+			dependency 'javax.validation:validation-api:1.1.0.Final'
 			dependency 'junit:junit:4.12'
 			dependency 'org.hamcrest:hamcrest-core:1.3'
 			dependency 'org.hamcrest:hamcrest-library:1.3'
+			dependency 'org.hibernate:hibernate-validator:5.2.1.Final'
 			dependency 'org.mockito:mockito-core:1.10.19'
 			dependency 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE'
 			dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644'
diff --git a/docs/build.gradle b/docs/build.gradle
index 92d3a8970..b3184452f 100644
--- a/docs/build.gradle
+++ b/docs/build.gradle
@@ -6,6 +6,7 @@ dependencies {
 	compile 'org.springframework:spring-webmvc'
 
 	testCompile project(':spring-restdocs')
+	testCompile 'javax.validation:validation-api'
 	testCompile 'junit:junit'
 	testCompile 'org.springframework:spring-test'
 }
diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc
index d7fa3f753..c4352df42 100644
--- a/docs/src/docs/asciidoc/documenting-your-api.adoc
+++ b/docs/src/docs/asciidoc/documenting-your-api.adoc
@@ -261,6 +261,92 @@ TIP: To make the path parameters available for documentation, the request must b
 built using one of the methods on `RestDocumentationRequestBuilders` rather than
 `MockMvcRequestBuilders`.
 
+
+
+[[documenting-your-api-constraints]]
+=== Documenting constraints
+
+Spring REST Docs provides a number of classes that can help you to document constraints.
+An instance of `ConstraintDescriptions` can be used to access descriptions of a class's
+constraints:
+
+[source,java,indent=0]
+----
+include::{examples-dir}/com/example/Constraints.java[tags=constraints]
+----
+<1> Create an instance of `ConstraintDescriptions` for the `UserInput` class
+<2> Get the descriptions of the name property's constraints. This list will contain two
+	descriptions; one for the `NotNull` constraint and one for the `Size` constraint.
+
+The `ApiDocumentation` class in the Spring HATEOAS sample shows this functionality in
+action.
+
+
+
+[[documenting-your-api-constraints-finding]]
+==== Finding constraints
+
+By default, constraints are found using a Bean Validation `Validator`. Currently, only
+property constraints are supported. You can customize the `Validator` that's used by
+creating `ConstraintDescriptions` with a custom `ValidatorConstraintResolver` instance.
+To take complete control of constraint resolution, your own implementation of
+`ConstraintResolver` can be used.
+
+
+
+[[documenting-your-api-constraints-describing]]
+==== Describing constraints
+
+Default descriptions are provided for all of the Bean Validation 1.1's constraints:
+
+* AssertFalse
+* AssertTrue
+* DecimalMax
+* DecimalMin
+* Digits
+* Future
+* Max
+* Min
+* NotNull
+* Null
+* Past
+* Pattern
+* Size
+
+To override the default descriptions, or to provide a new description, create a resource
+bundle with the base name
+`org.springframework.restdocs.constraints.ConstraintDescriptions`.  The Spring
+HATEOAS-based sample contains
+{samples}/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an
+example of such a resource bundle].
+
+Each key in the resource bundle is the fully-qualified name of a constraint plus
+`.description`. For example, the key for the standard `@NotNull` constraint is
+`javax.validation.constraints.NotNull.description`.
+
+Property placeholder's referring to a constraint's attributes can be used in its
+description. For example, the default description of the `@Min` constraint,
+`Must be at least ${value}`, refers to the constraint's `value` attribute.
+
+To take more control of constraint description resolution, create `ConstraintDescriptions`
+with a custom `ResourceBundleConstraintDescriptionResolver`. To take complete control,
+create `ConstraintDescriptions` with a custom `ConstraintDescriptionResolver`
+implementation.
+
+
+
+==== Using constraint descriptions in generated snippets
+
+Once you have a constraint's descriptions, you're free to use them however you like in
+the generated snippets. For example, you may want to include the constraint descriptions
+as part of a field's description. Alternatively, you could include the constraints as
+<> in
+the request fields snippet. The
+{samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`]
+class in the Spring HATEOAS-based sample illustrates the latter approach.
+
+
+
 [[documenting-your-api-default-snippets]]
 === Default snippets
 
diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java
new file mode 100644
index 000000000..6370b30cf
--- /dev/null
+++ b/docs/src/test/java/com/example/Constraints.java
@@ -0,0 +1,31 @@
+package com.example;
+
+import java.util.List;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+import org.springframework.restdocs.constraints.ConstraintDescriptions;
+
+public class Constraints {
+
+	@SuppressWarnings("unused")
+	// tag::constraints[]
+	public void example() {
+		ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); // <1>
+		List descriptions = userConstraints.descriptionsForProperty("name"); // <2>
+	}
+	
+	static class UserInput {
+		
+		@NotNull
+		@Size(min = 1)
+		String name;
+		
+		@NotNull
+		@Size(min = 8)
+		String password;
+	}
+	// end::constraints[]
+	
+}
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java
deleted file mode 100644
index f79aecdb4..000000000
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractNoteInput.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2014 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 com.example.notes;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.List;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-abstract class AbstractNoteInput {
-
-	private final String title;
-
-	private final String body;
-
-	private final List tagUris;
-
-	public AbstractNoteInput(String title, String body, List tagUris) {
-		this.title = title;
-		this.body = body;
-		this.tagUris = tagUris == null ? Collections. emptyList() : tagUris;
-	}
-
-	public String getTitle() {
-		return title;
-	}
-
-	public String getBody() {
-		return body;
-	}
-
-	@JsonProperty("tags")
-	public List getTagUris() {
-		return this.tagUris;
-	}
-}
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java
index 95e0dbafd..27b04f7b5 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java
+++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java
@@ -17,6 +17,7 @@
 package com.example.notes;
 
 import java.net.URI;
+import java.util.Collections;
 import java.util.List;
 
 import org.hibernate.validator.constraints.NotBlank;
@@ -24,11 +25,34 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class NoteInput extends AbstractNoteInput {
+public class NoteInput {
+	
+	@NotBlank
+	private final String title;
+
+	private final String body;
+
+	private final List tagUris;
 
 	@JsonCreator
-	public NoteInput(@NotBlank @JsonProperty("title") String title,
+	public NoteInput(@JsonProperty("title") String title,
 			@JsonProperty("body") String body, @JsonProperty("tags") List tagUris) {
-		super(title, body, tagUris);
+		this.title = title;
+		this.body = body;
+		this.tagUris = tagUris == null ? Collections.emptyList() : tagUris;
 	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public String getBody() {
+		return body;
+	}
+
+	@JsonProperty("tags")
+	public List getTagUris() {
+		return this.tagUris;
+	}
+
 }
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java
index 1921e724e..62e07d012 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java
+++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java
@@ -17,16 +17,39 @@
 package com.example.notes;
 
 import java.net.URI;
+import java.util.Collections;
 import java.util.List;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class NotePatchInput extends AbstractNoteInput {
+public class NotePatchInput {
+	
+	@NullOrNotBlank 
+	private final String title;
+	
+	private final String body;
+	
+	private final List tagUris;
 
 	@JsonCreator
-	public NotePatchInput(@NullOrNotBlank @JsonProperty("title") String title,
+	public NotePatchInput(@JsonProperty("title") String title,
 			@JsonProperty("body") String body, @JsonProperty("tags") List tagUris) {
-		super(title, body, tagUris);
+		this.title = title;
+		this.body = body;
+		this.tagUris = tagUris == null ? Collections.emptyList() : tagUris;
+	}
+	
+	public String getTitle() {
+		return title;
+	}
+
+	public String getBody() {
+		return body;
+	}
+
+	@JsonProperty("tags")
+	public List getTagUris() {
+		return this.tagUris;
 	}
 }
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java
index a60e035d4..7421a0ea0 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java
+++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java
@@ -17,18 +17,30 @@
 package com.example.notes;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-import javax.validation.constraints.NotNull;
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import javax.validation.constraints.Null;
 
 import org.hibernate.validator.constraints.CompositionType;
 import org.hibernate.validator.constraints.ConstraintComposition;
 import org.hibernate.validator.constraints.NotBlank;
 
 @ConstraintComposition(CompositionType.OR)
-@NotNull
+@Constraint(validatedBy = {})
+@Null
 @NotBlank
 @Target({ ElementType.FIELD, ElementType.PARAMETER })
+@Retention(RetentionPolicy.RUNTIME)
 public @interface NullOrNotBlank {
 
+	String message() default "Must be null or not blank";
+
+	Class[] groups() default {};
+
+	Class[] payload() default {};
+
 }
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java
index 8c3c3d393..2df23e5fd 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java
+++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java
@@ -21,10 +21,18 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class TagInput extends AbstractTagInput {
+public class TagInput {
+
+	@NotBlank
+	private final String name;
 
 	@JsonCreator
 	public TagInput(@NotBlank @JsonProperty("name") String name) {
-		super(name);
+		this.name = name;
+	}
+
+	public String getName() {
+		return name;
 	}
+
 }
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java
index 254a7886b..ba5b2fad0 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java
+++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java
@@ -19,10 +19,18 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class TagPatchInput extends AbstractTagInput {
+public class TagPatchInput {
+	
+	@NullOrNotBlank
+	private final String name;
 
 	@JsonCreator
 	public TagPatchInput(@NullOrNotBlank @JsonProperty("name") String name) {
-		super(name);
+		this.name = name;
 	}
-}
+
+	public String getName() {
+		return name;
+	}
+	
+}
\ No newline at end of file
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 1bf108109..06e22615b 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,14 +20,15 @@
 import static org.hamcrest.Matchers.notNullValue;
 import static org.springframework.restdocs.RestDocumentation.document;
 import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
+import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
 import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
 import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
 import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
 import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
 import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
-import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
-import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
-import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.snippet.Attributes.key;
 import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -44,11 +45,14 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.SpringApplicationConfiguration;
 import org.springframework.hateoas.MediaTypes;
+import org.springframework.restdocs.constraints.ConstraintDescriptions;
+import org.springframework.restdocs.payload.FieldDescriptor;
 import org.springframework.restdocs.payload.JsonFieldType;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.util.StringUtils;
 import org.springframework.web.context.WebApplicationContext;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -147,15 +151,17 @@ public void notesCreateExample() throws Exception {
 		note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html");
 		note.put("tags", Arrays.asList(tagLocation));
 
+		ConstrainedFields fields = new ConstrainedFields(NoteInput.class);
+
 		this.mockMvc.perform(
 				post("/notes").contentType(MediaTypes.HAL_JSON).content(
 						this.objectMapper.writeValueAsString(note)))
 				.andExpect(status().isCreated())
 				.andDo(document("notes-create-example",
 						requestFields(
-								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"))));
+								fields.withPath("title").description("The title of the note"),
+								fields.withPath("body").description("The body of the note"),
+								fields.withPath("tags").description("An array of tag resource URIs"))));
 	}
 
 	@Test
@@ -220,13 +226,15 @@ public void tagsCreateExample() throws Exception {
 		Map tag = new HashMap();
 		tag.put("name", "REST");
 
+		ConstrainedFields fields = new ConstrainedFields(TagInput.class);
+
 		this.mockMvc.perform(
 				post("/tags").contentType(MediaTypes.HAL_JSON).content(
 						this.objectMapper.writeValueAsString(tag)))
 				.andExpect(status().isCreated())
 				.andDo(document("tags-create-example",
 						requestFields(
-								fieldWithPath("name").description("The name of the tag"))));
+								fields.withPath("name").description("The name of the tag"))));
 	}
 
 	@Test
@@ -261,15 +269,24 @@ public void noteUpdateExample() throws Exception {
 		Map noteUpdate = new HashMap();
 		noteUpdate.put("tags", Arrays.asList(tagLocation));
 
+		ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class);
+
 		this.mockMvc.perform(
 				patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
 						this.objectMapper.writeValueAsString(noteUpdate)))
 				.andExpect(status().isNoContent())
 				.andDo(document("note-update-example",
 						requestFields(
-								fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(),
-								fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(),
-								fieldWithPath("tags").description("An array of tag resource URIs").optional())));
+								fields.withPath("title")
+										.description("The title of the note")
+										.type(JsonFieldType.STRING)
+										.optional(),
+								fields.withPath("body")
+										.description("The body of the note")
+										.type(JsonFieldType.STRING)
+										.optional(),
+								fields.withPath("tags")
+										.description("An array of tag resource URIs"))));
 	}
 
 	@Test
@@ -311,13 +328,16 @@ public void tagUpdateExample() throws Exception {
 		Map tagUpdate = new HashMap();
 		tagUpdate.put("name", "RESTful");
 
+		ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class);
+
 		this.mockMvc.perform(
 				patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
 						this.objectMapper.writeValueAsString(tagUpdate)))
 				.andExpect(status().isNoContent())
 				.andDo(document("tag-update-example",
 						requestFields(
-								fieldWithPath("name").description("The name of the tag"))));
+								fields.withPath("name")
+										.description("The name of the tag"))));
 	}
 
 	private void createNote(String title, String body) {
@@ -333,4 +353,20 @@ private void createTag(String name) {
 		tag.setName(name);
 		this.tagRepository.save(tag);
 	}
+
+	private static class ConstrainedFields {
+
+		private final ConstraintDescriptions constraintDescriptions;
+
+		ConstrainedFields(Class input) {
+			this.constraintDescriptions = new ConstraintDescriptions(input);
+		}
+
+		private FieldDescriptor withPath(String path) {
+			return fieldWithPath(path).attributes(key("constraints").value(StringUtils
+					.collectionToDelimitedString(this.constraintDescriptions
+							.descriptionsForProperty(path), ". ")));
+		}
+	}
+
 }
diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java
new file mode 100644
index 000000000..24d9e85ad
--- /dev/null
+++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java
@@ -0,0 +1,70 @@
+/*
+ * 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 com.example.notes;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import org.junit.Test;
+
+
+public class NullOrNotBlankTests {
+
+	private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+	@Test
+	public void nullValue() {
+		Set> violations = validator.validate(new Constrained(null));
+		assertThat(violations.size(), is(0));
+	}
+
+	@Test
+	public void zeroLengthValue() {
+		Set> violations = validator.validate(new Constrained(""));
+		assertThat(violations.size(), is(2));
+	}
+
+	@Test
+	public void blankValue() {
+		Set> violations = validator.validate(new Constrained("   "));
+		assertThat(violations.size(), is(2));
+	}
+
+	@Test
+	public void nonBlankValue() {
+		Set> violations = validator.validate(new Constrained("test"));
+		assertThat(violations.size(), is(0));
+	}
+
+	static class Constrained {
+
+		@NullOrNotBlank
+		private final String value;
+
+		public Constrained(String value) {
+			this.value = value;
+		}
+
+	}
+
+}
diff --git a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties
new file mode 100644
index 000000000..433d946d5
--- /dev/null
+++ b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties
@@ -0,0 +1,2 @@
+com.example.notes.NullOrNotBlank.description=Must be null or not blank
+org.hibernate.validator.constraints.NotBlank.description=Must not be blank
\ No newline at end of file
diff --git a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet
new file mode 100644
index 000000000..cd1e825c8
--- /dev/null
+++ b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet
@@ -0,0 +1,11 @@
+|===
+|Path|Type|Description|Constraints
+
+{{#fields}}
+|{{path}}
+|{{type}}
+|{{description}}
+|{{constraints}}
+
+{{/fields}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs/build.gradle b/spring-restdocs/build.gradle
index e4c2fd502..bf1fc8348 100644
--- a/spring-restdocs/build.gradle
+++ b/spring-restdocs/build.gradle
@@ -33,16 +33,9 @@ configurations {
 	jmustache
 }
 
-ext {
-	javadocLinks = [
-		'http://docs.oracle.com/javase/8/docs/api/',
-		"http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/"
-	] as String[]
-}
-
 task jmustacheRepackJar(type: Jar) { repackJar ->
 	repackJar.baseName = "restdocs-jmustache-repack"
-	repackJar.version = jmustacheVersion
+	repackJar.version = dependencyManagement.managedVersions['com.samskivert:jmustache']
 
 	doLast() {
 		project.ant {
@@ -70,10 +63,13 @@ dependencies {
 	jacoco 'org.jacoco:org.jacoco.agent::runtime'
 	jarjar 'com.googlecode.jarjar:jarjar:1.3'
 	jmustache 'com.samskivert:jmustache@jar'
+	optional 'javax.validation:validation-api'
 	testCompile 'org.springframework.hateoas:spring-hateoas'
 	testCompile 'org.mockito:mockito-core'
 	testCompile 'org.hamcrest:hamcrest-core'
 	testCompile 'org.hamcrest:hamcrest-library'
+	testCompile 'org.hibernate:hibernate-validator'
+	testRuntime 'org.glassfish:javax.el:3.0.0'
 }
 
 jar {
@@ -94,7 +90,11 @@ javadoc {
 	options.author = true
 	options.header = "Spring REST Docs $version"
 	options.docTitle = "${options.header} API"
-	options.links(project.ext.javadocLinks)
+	options.links = [
+		'http://docs.oracle.com/javase/8/docs/api/',
+		"http://docs.spring.io/spring-framework/docs/${dependencyManagement.managedVersions['org.springframework:spring-core']}/javadoc-api/",
+		'https://docs.jboss.org/hibernate/stable/beanvalidation/api/'
+	] as String[]
 	options.addStringOption '-quiet'
 }
 
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java
new file mode 100644
index 000000000..1adf117ef
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java
@@ -0,0 +1,62 @@
+/*
+ * 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.constraints;
+
+import java.util.Map;
+
+/**
+ * A constraint
+ * 
+ * @author Andy Wilkinson
+ */
+public class Constraint {
+
+	private final String name;
+
+	private final Map configuration;
+
+	/**
+	 * Creates a new {@code Constraint} with the given {@code name} and
+	 * {@code configuration}.
+	 * 
+	 * @param name the name
+	 * @param configuration the configuration
+	 */
+	public Constraint(String name, Map configuration) {
+		this.name = name;
+		this.configuration = configuration;
+	}
+
+	/**
+	 * Returns the name of the constraint
+	 * 
+	 * @return the name
+	 */
+	public String getName() {
+		return this.name;
+	}
+
+	/**
+	 * Returns the configuration of the constraint
+	 * 
+	 * @return the configuration
+	 */
+	public Map getConfiguration() {
+		return this.configuration;
+	}
+
+}
diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java
similarity index 56%
rename from samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java
rename to spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java
index 824ce43b6..7a428e3f6 100644
--- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/AbstractTagInput.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014 the original author or authors.
+ * 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.
@@ -14,18 +14,21 @@
  * limitations under the License.
  */
 
-package com.example.notes;
+package org.springframework.restdocs.constraints;
 
+/**
+ * Resolves a description for a {@link Constraint}.
+ * 
+ * @author Andy Wilkinson
+ *
+ */
+public interface ConstraintDescriptionResolver {
 
-abstract class AbstractTagInput {
-
-	private final String name;
-
-	public AbstractTagInput(String name) {
-		this.name = name;
-	}
-
-	public String getName() {
-		return name;
-	}
+	/**
+	 * Resolves the description for the given {@code constraint}.
+	 * 
+	 * @param constraint the constraint
+	 * @return the description
+	 */
+	public String resolveDescription(Constraint constraint);
 }
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java
new file mode 100644
index 000000000..0161b6d57
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java
@@ -0,0 +1,108 @@
+/*
+ * 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.constraints;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides access to descriptions of a class's constraints
+ * 
+ * @author Andy Wilkinson
+ */
+public class ConstraintDescriptions {
+
+	private final Class clazz;
+
+	private final ConstraintResolver constraintResolver;
+
+	private final ConstraintDescriptionResolver descriptionResolver;
+
+	/**
+	 * Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
+	 * Constraints will be resolved using a {@link ValidatorConstraintResolver} and
+	 * descriptions will be resolved using a
+	 * {@link ResourceBundleConstraintDescriptionResolver}.
+	 * 
+	 * @param clazz the class
+	 */
+	public ConstraintDescriptions(Class clazz) {
+		this(clazz, new ValidatorConstraintResolver(),
+				new ResourceBundleConstraintDescriptionResolver());
+	}
+
+	/**
+	 * Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
+	 * Constraints will be resolved using the given {@link constraintResolver} and
+	 * descriptions will be resolved using a
+	 * {@link ResourceBundleConstraintDescriptionResolver}.
+	 * 
+	 * @param clazz the class
+	 * @param constraintResolver the constraint resolver
+	 */
+	public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResolver) {
+		this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver());
+	}
+
+	/**
+	 * Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
+	 * Constraints will be resolved using a {@link ValidatorConstraintResolver} and
+	 * descriptions will be resolved using the given {@code descriptionResolver}.
+	 * 
+	 * @param clazz the class
+	 * @param descriptionResolver the description resolver
+	 */
+	public ConstraintDescriptions(Class clazz,
+			ConstraintDescriptionResolver descriptionResolver) {
+		this(clazz, new ValidatorConstraintResolver(), descriptionResolver);
+	}
+
+	/**
+	 * Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
+	 * Constraints will be resolved using the given {@code constraintResolver} and
+	 * descriptions will be resolved using the given {@code descriptionResolver}.
+	 * 
+	 * @param clazz the class
+	 * @param constraintResolver the constraint resolver
+	 * @param descriptionResolver the description resolver
+	 */
+	public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResolver,
+			ConstraintDescriptionResolver descriptionResolver) {
+		this.clazz = clazz;
+		this.constraintResolver = constraintResolver;
+		this.descriptionResolver = descriptionResolver;
+	}
+
+	/**
+	 * Returns a list of the descriptions for the constraints on the given property
+	 * 
+	 * @param property the property
+	 * @return the list of constraint descriptions
+	 */
+	public List descriptionsForProperty(String property) {
+		List constraints = this.constraintResolver.resolveForProperty(
+				property, this.clazz);
+		List descriptions = new ArrayList<>();
+		for (Constraint constraint : constraints) {
+			descriptions.add(this.descriptionResolver.resolveDescription(constraint));
+		}
+		Collections.sort(descriptions);
+		return descriptions;
+	}
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java
new file mode 100644
index 000000000..8f5c6580e
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java
@@ -0,0 +1,38 @@
+/*
+ * 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.constraints;
+
+import java.util.List;
+
+/**
+ * An abstraction for resolving a class's constraints.
+ * 
+ * @author Andy Wilkinson
+ */
+public interface ConstraintResolver {
+
+	/**
+	 * Resolves and returns the constraints for the given {@code property} on the given
+	 * {@code clazz}. If there are no constraints, an empty list is returned.
+	 * 
+	 * @param property the property
+	 * @param clazz the class
+	 * @return the list of constraints, never {@code null}
+	 */
+	List resolveForProperty(String property, Class clazz);
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java
new file mode 100644
index 000000000..7ea94a8d6
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java
@@ -0,0 +1,145 @@
+/*
+ * 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.constraints;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.validation.constraints.AssertFalse;
+import javax.validation.constraints.AssertTrue;
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.Future;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Null;
+import javax.validation.constraints.Past;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+import org.springframework.util.PropertyPlaceholderHelper;
+import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
+
+/**
+ * A {@link ConstraintDescriptionResolver} that resolves constraint descriptions from a
+ * {@link ResourceBundle}. The resource bundle's keys are the name of the constraint with
+ * {@code .description} appended. For example, the key for the constraint named
+ * {@code javax.validation.constraints.NotNull} is
+ * {@code javax.validation.constraints.NotNull.description}.
+ * 

+ * Default descriptions are provided for Bean Validation 1.1's constraints: + * + *

    + *
  • {@link AssertFalse} + *
  • {@link AssertTrue} + *
  • {@link DecimalMax} + *
  • {@link DecimalMin} + *
  • {@link Digits} + *
  • {@link Future} + *
  • {@link Max} + *
  • {@link Min} + *
  • {@link NotNull} + *
  • {@link Null} + *
  • {@link Past} + *
  • {@link Pattern} + *
  • {@link Size} + *
+ * + * @author Andy Wilkinson + */ +public class ResourceBundleConstraintDescriptionResolver implements + ConstraintDescriptionResolver { + + private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( + "${", "}"); + + private final ResourceBundle defaultDescriptions; + + private final ResourceBundle userDescriptions; + + /** + * Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve + * descriptions by looking them up in a resource bundle with the base name + * {@code org.springframework.restdocs.constraints.ConstraintDescriptions} in the + * default locale loaded using the thread context class loader + */ + public ResourceBundleConstraintDescriptionResolver() { + this(getBundle("ConstraintDescriptions")); + } + + /** + * Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve + * descriptions by looking them up in the given {@code resourceBundle}. + * + * @param resourceBundle the resource bundle + */ + public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) { + this.defaultDescriptions = getBundle("DefaultConstraintDescriptions"); + this.userDescriptions = resourceBundle; + } + + private static ResourceBundle getBundle(String name) { + try { + return ResourceBundle.getBundle( + ResourceBundleConstraintDescriptionResolver.class.getPackage() + .getName() + "." + name, Locale.getDefault(), Thread + .currentThread().getContextClassLoader()); + } + catch (MissingResourceException ex) { + return null; + } + } + + @Override + public String resolveDescription(Constraint constraint) { + String key = constraint.getName() + ".description"; + return this.propertyPlaceholderHelper.replacePlaceholders(getDescription(key), + new ConstraintPlaceholderResolver(constraint)); + } + + private String getDescription(String key) { + try { + if (this.userDescriptions != null) { + return this.userDescriptions.getString(key); + } + } + catch (MissingResourceException ex) { + // Continue and return default description, if available + } + return this.defaultDescriptions.getString(key); + } + + private static class ConstraintPlaceholderResolver implements PlaceholderResolver { + + private final Constraint constraint; + + private ConstraintPlaceholderResolver(Constraint constraint) { + this.constraint = constraint; + } + + @Override + public String resolvePlaceholder(String placeholderName) { + Object replacement = this.constraint.getConfiguration().get(placeholderName); + return replacement != null ? replacement.toString() : null; + } + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java new file mode 100644 index 000000000..2a638ed62 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -0,0 +1,82 @@ +/* + * 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.constraints; + +import java.util.ArrayList; +import java.util.List; + +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import javax.validation.constraints.NotNull; +import javax.validation.metadata.BeanDescriptor; +import javax.validation.metadata.ConstraintDescriptor; +import javax.validation.metadata.PropertyDescriptor; + +/** + * A {@link ConstraintResolver} that uses a Bean Validation {@link Validator} to resolve + * constraints. The name of the constraint is the fully-qualified class name of the + * constraint annotation. For example, a {@link NotNull} constraint will be named + * {@code javax.validation.constraints.NotNull}. + * + * @author Andy Wilkinson + * + */ +public class ValidatorConstraintResolver implements ConstraintResolver { + + private final Validator validator; + + /** + * Creates a new {@code ValidatorConstraintResolver} that will use a {@link Validator} + * in its default configurationto resolve constraints. + * + * @see Validation#buildDefaultValidatorFactory() + * @see ValidatorFactory#getValidator() + */ + public ValidatorConstraintResolver() { + this(Validation.buildDefaultValidatorFactory().getValidator()); + } + + /** + * Creates a new {@code ValidatorConstraintResolver} that will use the given + * {@code Validator} to resolve constraints. + * + * @param validator the validator + */ + public ValidatorConstraintResolver(Validator validator) { + this.validator = validator; + } + + @Override + public List resolveForProperty(String property, Class clazz) { + List constraints = new ArrayList<>(); + BeanDescriptor beanDescriptor = this.validator.getConstraintsForClass(clazz); + PropertyDescriptor propertyDescriptor = beanDescriptor + .getConstraintsForProperty(property); + if (propertyDescriptor != null) { + for (ConstraintDescriptor constraintDescriptor : propertyDescriptor + .getConstraintDescriptors()) { + constraints + .add(new Constraint(constraintDescriptor.getAnnotation() + .annotationType().getName(), constraintDescriptor + .getAttributes())); + } + } + return constraints; + } + +} diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties new file mode 100644 index 000000000..c945d4865 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -0,0 +1,13 @@ +javax.validation.constraints.AssertFalse.description=Must be false +javax.validation.constraints.AssertTrue.description=Must be true +javax.validation.constraints.DecimalMax.description=Must be at most ${value} +javax.validation.constraints.DecimalMin.description=Must be at least ${value} +javax.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits +javax.validation.constraints.Future.description=Must be in the future +javax.validation.constraints.Max.description=Must be at most ${value} +javax.validation.constraints.Min.description=Must be at least ${value} +javax.validation.constraints.NotNull.description=Must not be null +javax.validation.constraints.Null.description=Must be null +javax.validation.constraints.Past.description=Must be in the past +javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' +javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 7f8cb279e..194d0bd7b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -329,6 +329,7 @@ public ResponseEntity> foo() { return new ResponseEntity>(response, headers, HttpStatus.OK); } + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java new file mode 100644 index 000000000..8aa889891 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -0,0 +1,182 @@ +package org.springframework.restdocs.constraints; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.validation.constraints.AssertFalse; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Digits; +import javax.validation.constraints.Future; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import javax.validation.constraints.Past; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import org.junit.Test; + +/** + * Tests for {@link ConstraintDescriptions} + * + * @author Andy Wilkinson + */ +public class ConstraintDescriptionsTests { + + private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( + Constrained.class); + + @Test + public void assertFalse() { + assertThat(this.constraintDescriptions.descriptionsForProperty("assertFalse"), + contains("Must be false")); + } + + @Test + public void assertTrue() { + assertThat(this.constraintDescriptions.descriptionsForProperty("assertTrue"), + contains("Must be true")); + } + + @Test + public void decimalMax() { + assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMax"), + contains("Must be at most 9.875")); + } + + @Test + public void decimalMin() { + assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMin"), + contains("Must be at least 1.5")); + } + + @Test + public void digits() { + assertThat(this.constraintDescriptions.descriptionsForProperty("digits"), + contains("Must have at most 2 integral digits and 5 fractional digits")); + } + + @Test + public void future() { + assertThat(this.constraintDescriptions.descriptionsForProperty("future"), + contains("Must be in the future")); + } + + @Test + public void max() { + assertThat(this.constraintDescriptions.descriptionsForProperty("max"), + contains("Must be at most 10")); + } + + @Test + public void min() { + assertThat(this.constraintDescriptions.descriptionsForProperty("min"), + contains("Must be at least 5")); + } + + @Test + public void notNull() { + assertThat(this.constraintDescriptions.descriptionsForProperty("notNull"), + contains("Must not be null")); + } + + @Test + public void nul() { + assertThat(this.constraintDescriptions.descriptionsForProperty("nul"), + contains("Must be null")); + } + + @Test + public void past() { + assertThat(this.constraintDescriptions.descriptionsForProperty("past"), + contains("Must be in the past")); + } + + @Test + public void pattern() { + assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), + contains("Must match the regular expression '[A-Z][a-z]+'")); + } + + @Test + public void size() { + assertThat(this.constraintDescriptions.descriptionsForProperty("size"), + contains("Size must be between 0 and 10 inclusive")); + } + + @Test + public void sizeList() { + assertThat( + this.constraintDescriptions.descriptionsForProperty("sizeList"), + contains("Size must be between 1 and 4 inclusive", + "Size must be between 8 and 10 inclusive")); + } + + @Test + public void unconstrained() { + assertThat(this.constraintDescriptions.descriptionsForProperty("unconstrained"), + hasSize(0)); + } + + @Test + public void nonExistentProperty() { + assertThat(this.constraintDescriptions.descriptionsForProperty("doesNotExist"), + hasSize(0)); + } + + private static class Constrained { + + @AssertFalse + private boolean assertFalse; + + @AssertTrue + private boolean assertTrue; + + @DecimalMax("9.875") + private BigDecimal decimalMax; + + @DecimalMin("1.5") + private BigDecimal decimalMin; + + @Digits(fraction = 5, integer = 2) + private BigDecimal digits; + + @Future + private Date future; + + @NotNull + private String notNull; + + @Max(10) + private int max; + + @Min(5) + private int min; + + @Null + private String nul; + + @Past + private Date past; + + @Pattern(regexp = "[A-Z][a-z]+") + private String pattern; + + @Size(min = 0, max = 10) + private String size; + + @Size.List({ @Size(min = 1, max = 4), @Size(min = 8, max = 10) }) + private String sizeList; + + @SuppressWarnings("unused") + private String unconstrained; + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java new file mode 100644 index 000000000..03202540e --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -0,0 +1,192 @@ +package org.springframework.restdocs.constraints; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.ListResourceBundle; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.validation.constraints.AssertFalse; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Digits; +import javax.validation.constraints.Future; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import javax.validation.constraints.Past; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import org.junit.Test; + +/** + * Tests for {@link ResourceBundleConstraintDescriptionResolver} + * + * @author Andy Wilkinson + */ +public class ResourceBundleConstraintDescriptionResolverTests { + + private final ResourceBundleConstraintDescriptionResolver resolver = new ResourceBundleConstraintDescriptionResolver(); + + @Test + public void defaultMessageAssertFalse() { + String description = this.resolver.resolveDescription(new Constraint( + AssertFalse.class.getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must be false"))); + } + + @Test + public void defaultMessageAssertTrue() { + String description = this.resolver.resolveDescription(new Constraint( + AssertTrue.class.getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must be true"))); + } + + @Test + public void defaultMessageDecimalMax() { + Map configuration = new HashMap<>(); + configuration.put("value", "9.875"); + String description = this.resolver.resolveDescription(new Constraint( + DecimalMax.class.getName(), configuration)); + assertThat(description, is(equalTo("Must be at most 9.875"))); + } + + @Test + public void defaultMessageDecimalMin() { + Map configuration = new HashMap<>(); + configuration.put("value", "1.5"); + String description = this.resolver.resolveDescription(new Constraint( + DecimalMin.class.getName(), configuration)); + assertThat(description, is(equalTo("Must be at least 1.5"))); + } + + @Test + public void defaultMessageDigits() { + Map configuration = new HashMap<>(); + configuration.put("integer", "2"); + configuration.put("fraction", "5"); + String description = this.resolver.resolveDescription(new Constraint(Digits.class + .getName(), configuration)); + assertThat(description, is(equalTo("Must have at most 2 integral digits and 5 " + + "fractional digits"))); + } + + @Test + public void defaultMessageFuture() { + String description = this.resolver.resolveDescription(new Constraint(Future.class + .getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must be in the future"))); + } + + @Test + public void defaultMessageMax() { + Map configuration = new HashMap<>(); + configuration.put("value", 10); + String description = this.resolver.resolveDescription(new Constraint(Max.class + .getName(), configuration)); + assertThat(description, is(equalTo("Must be at most 10"))); + } + + @Test + public void defaultMessageMin() { + Map configuration = new HashMap<>(); + configuration.put("value", 10); + String description = this.resolver.resolveDescription(new Constraint(Min.class + .getName(), configuration)); + assertThat(description, is(equalTo("Must be at least 10"))); + } + + @Test + public void defaultMessageNotNull() { + String description = this.resolver.resolveDescription(new Constraint( + NotNull.class.getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must not be null"))); + } + + @Test + public void defaultMessageNull() { + String description = this.resolver.resolveDescription(new Constraint(Null.class + .getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must be null"))); + } + + @Test + public void defaultMessagePast() { + String description = this.resolver.resolveDescription(new Constraint(Past.class + .getName(), Collections. emptyMap())); + assertThat(description, is(equalTo("Must be in the past"))); + } + + @Test + public void defaultMessagePattern() { + Map configuration = new HashMap<>(); + configuration.put("regexp", "[A-Z][a-z]+"); + String description = this.resolver.resolveDescription(new Constraint( + Pattern.class.getName(), configuration)); + assertThat(description, + is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); + } + + @Test + public void defaultMessageSize() { + Map configuration = new HashMap<>(); + configuration.put("min", 2); + configuration.put("max", 10); + String description = this.resolver.resolveDescription(new Constraint(Size.class + .getName(), configuration)); + assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive"))); + } + + @Test + public void customMessage() { + Thread.currentThread().setContextClassLoader(new ClassLoader() { + + @Override + public URL getResource(String name) { + if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) { + return super + .getResource("org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); + } + return super.getResource(name); + } + + }); + + try { + String description = new ResourceBundleConstraintDescriptionResolver() + .resolveDescription(new Constraint(NotNull.class.getName(), + Collections. emptyMap())); + assertThat(description, is(equalTo("Should not be null"))); + + } + finally { + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + } + } + + @Test + public void customResourceBundle() { + ResourceBundle bundle = new ListResourceBundle() { + + @Override + protected Object[][] getContents() { + return new String[][] { { NotNull.class.getName() + ".description", + "Not null" } }; + } + + }; + String description = new ResourceBundleConstraintDescriptionResolver(bundle) + .resolveDescription(new Constraint(NotNull.class.getName(), Collections + . emptyMap())); + assertThat(description, is(equalTo("Not null"))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java new file mode 100644 index 000000000..12accc246 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -0,0 +1,97 @@ +package org.springframework.restdocs.constraints; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +import javax.validation.Payload; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.CompositionType; +import org.hibernate.validator.constraints.ConstraintComposition; +import org.hibernate.validator.constraints.NotBlank; +import org.junit.Test; + +/** + * Tests for {@link ValidatorConstraintResolver} + * + * @author Andy Wilkinson + */ +public class ValidatorConstraintResolverTests { + + private final ValidatorConstraintResolver resolver = new ValidatorConstraintResolver(); + + @Test + public void singleFieldConstraint() { + List constraints = this.resolver.resolveForProperty("single", + ConstrainedFields.class); + assertThat(constraints, hasSize(1)); + assertThat(constraints.get(0).getName(), is(NotNull.class.getName())); + } + + @Test + public void multipleFieldConstraints() { + List constraints = this.resolver.resolveForProperty("multiple", + ConstrainedFields.class); + assertThat(constraints, hasSize(2)); + assertThat(constraints.get(0).getName(), is(NotNull.class.getName())); + assertThat(constraints.get(1).getName(), is(Size.class.getName())); + assertThat(constraints.get(1).getConfiguration().get("min"), is((Object) 8)); + assertThat(constraints.get(1).getConfiguration().get("max"), is((Object) 16)); + } + + @Test + public void noFieldConstraints() { + List constraints = this.resolver.resolveForProperty("none", + ConstrainedFields.class); + assertThat(constraints, hasSize(0)); + } + + @Test + public void compositeConstraint() { + List constraints = this.resolver.resolveForProperty("composite", + ConstrainedFields.class); + assertThat(constraints, hasSize(1)); + } + + private static class ConstrainedFields { + + @NotNull + private String single; + + @NotNull + @Size(min = 8, max = 16) + private String multiple; + + @SuppressWarnings("unused") + private String none; + + @CompositeConstraint + private String composite; + } + + @ConstraintComposition(CompositionType.OR) + @Null + @NotBlank + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @javax.validation.Constraint(validatedBy = {}) + public @interface CompositeConstraint { + + String message() default "Must be null or not blank"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + } + +} diff --git a/spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties b/spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties new file mode 100644 index 000000000..ea0f3a91c --- /dev/null +++ b/spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties @@ -0,0 +1 @@ +javax.validation.constraints.NotNull.description=Should not be null \ No newline at end of file From 65e37a62769d51b94e8efe00e39d943583253dbb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Aug 2015 15:28:17 +0100 Subject: [PATCH 0117/1059] Update tests to cope with constraint ordering being unspecified --- .../ValidatorConstraintResolverTests.java | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 12accc246..26ab45cac 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -1,20 +1,27 @@ package org.springframework.restdocs.constraints; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; +import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.validation.Payload; import javax.validation.constraints.NotNull; import javax.validation.constraints.Null; import javax.validation.constraints.Size; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.hibernate.validator.constraints.CompositionType; import org.hibernate.validator.constraints.ConstraintComposition; import org.hibernate.validator.constraints.NotBlank; @@ -37,15 +44,16 @@ public void singleFieldConstraint() { assertThat(constraints.get(0).getName(), is(NotNull.class.getName())); } + @SuppressWarnings("unchecked") @Test public void multipleFieldConstraints() { List constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class); assertThat(constraints, hasSize(2)); - assertThat(constraints.get(0).getName(), is(NotNull.class.getName())); - assertThat(constraints.get(1).getName(), is(Size.class.getName())); - assertThat(constraints.get(1).getConfiguration().get("min"), is((Object) 8)); - assertThat(constraints.get(1).getConfiguration().get("max"), is((Object) 16)); + assertThat( + constraints, + containsInAnyOrder(constraint(NotNull.class), constraint(Size.class) + .config("min", 8).config("max", 16))); } @Test @@ -94,4 +102,47 @@ private static class ConstrainedFields { } + private ConstraintMatcher constraint(final Class annotation) { + return new ConstraintMatcher(annotation); + } + + private static class ConstraintMatcher extends BaseMatcher { + + private final Class annotation; + + private final Map configuration = new HashMap<>(); + + private ConstraintMatcher(Class annotation) { + this.annotation = annotation; + } + + public ConstraintMatcher config(String key, Object value) { + this.configuration.put(key, value); + return this; + } + + @Override + public boolean matches(Object item) { + if (!(item instanceof Constraint)) { + return false; + } + Constraint constraint = (Constraint) item; + if (!constraint.getName().equals(this.annotation.getName())) { + return false; + } + for (Entry entry : this.configuration.entrySet()) { + if (!constraint.getConfiguration().get(entry.getKey()) + .equals(entry.getValue())) { + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("Constraint named " + this.annotation.getName() + + " with configuration " + this.configuration); + } + } } From 3579b34a7793f9017aad06fc9c1183e9b98ec686 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Aug 2015 22:17:44 +0100 Subject: [PATCH 0118/1059] Introduce a new operation abstraction to reduce coupling with MockMvc Prior to this commit, MockMvc-classes were used throughout Spring REST Docs. For example, each Snippet was called with an MvcResult. This has proven problematic for a few reasons: 1. The MockMvc APIs aren't very amenable to modifying a request or response before it's documented. This caused the existing support for response modification to rely on CGLib proxies and method interceptors. A similary complex solution for request modifiction would also have been necessary. 2. Things are harder to reason about than they need to be as the MockHttpServletRequest and MockHttpServletResponse classes expose more than is required when generating API documentation. 3. Supporting other test frameworks, such as Rest Assured, is hard This commit introduces a new Operation abstract that encapsulates all of the information required to document the request that was sent and the response that was received when performing an operation on a RESTful service. The new abstraction uses types from Spring's Web support, such as HttpHeaders, RequestMethod, and MediaType, but does not rely on MockMvc. Closes gh-108 --- docs/src/docs/asciidoc/configuration.adoc | 6 +- .../com/example/CustomUriConfiguration.java | 3 +- .../test/java/com/example/PathParameters.java | 2 +- docs/src/test/java/com/example/Payload.java | 4 +- .../java/com/example/RequestParameters.java | 4 +- .../RestDocumentationResultHandler.java | 21 +- .../restdocs/config/UriConfigurer.java | 23 -- .../restdocs/curl/CurlRequestSnippet.java | 119 +++----- .../restdocs/http/HttpRequestSnippet.java | 97 +++--- .../restdocs/http/HttpResponseSnippet.java | 25 +- .../hypermedia/AbstractJsonLinkExtractor.java | 6 +- .../hypermedia/ContentTypeLinkExtractor.java | 14 +- .../restdocs/hypermedia/LinkExtractor.java | 9 +- .../restdocs/hypermedia/LinksSnippet.java | 8 +- .../MockMvcOperationRequestFactory.java | 162 ++++++++++ .../MockMvcOperationResponseFactory.java | 52 ++++ .../restdocs/operation/Operation.java | 56 ++++ .../restdocs/operation/OperationRequest.java | 79 +++++ .../operation/OperationRequestPart.java | 57 ++++ .../restdocs/operation/OperationResponse.java | 52 ++++ .../restdocs/operation/Parameters.java | 64 ++++ .../restdocs/operation/StandardOperation.java | 72 +++++ .../operation/StandardOperationRequest.java | 98 +++++++ .../StandardOperationRequestPart.java | 72 +++++ .../operation/StandardOperationResponse.java | 65 +++++ .../payload/AbstractFieldsSnippet.java | 33 +-- .../restdocs/payload/ContentHandler.java | 61 ++++ ...adHandler.java => JsonContentHandler.java} | 28 +- .../restdocs/payload/PayloadHandler.java | 45 --- .../payload/RequestFieldsSnippet.java | 16 +- .../payload/ResponseFieldsSnippet.java | 16 +- ...oadHandler.java => XmlContentHandler.java} | 34 ++- .../request/AbstractParametersSnippet.java | 31 +- .../request/PathParametersSnippet.java | 16 +- .../request/RequestParametersSnippet.java | 6 +- .../ContentModifyingReponsePostProcessor.java | 4 +- .../DocumentableHttpServletRequest.java | 276 ------------------ .../restdocs/snippet/Snippet.java | 10 +- .../restdocs/snippet/TemplatedSnippet.java | 20 +- .../RestDocumentationConfigurerTests.java | 22 -- .../curl/CurlRequestSnippetTests.java | 222 +++++--------- .../http/HttpRequestSnippetTests.java | 110 +++---- .../http/HttpResponseSnippetTests.java | 41 +-- .../ContentTypeLinkExtractorTests.java | 27 +- .../LinkExtractorsPayloadTests.java | 13 +- .../hypermedia/LinksSnippetTests.java | 68 ++--- .../MockMvcOperationRequestFactoryTests.java | 220 ++++++++++++++ .../payload/RequestFieldsSnippetTests.java | 107 +++---- .../payload/ResponseFieldsSnippetTests.java | 95 +++--- .../request/PathParametersSnippetTests.java | 54 ++-- .../RequestParametersSnippetTests.java | 66 ++--- ...ntModifyingResponsePostProcessorTests.java | 9 +- .../restdocs/test/OperationBuilder.java | 211 +++++++++++++ 53 files changed, 1931 insertions(+), 1100 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{JsonPayloadHandler.java => JsonContentHandler.java} (77%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{XmlPayloadHandler.java => XmlContentHandler.java} (79%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 9654df1ad..e7a0a3757 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -19,9 +19,6 @@ The default configuration for URIs documented by Spring REST Docs is: |Port |`8080` - -|Context path -|Empty string |=== This configuration is applied by `RestDocumentationConfigurer`. You can use its API to @@ -32,6 +29,9 @@ change one or more of the defaults to suit your needs: include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- +TIP: To configure a request's context path, use the `contextPath` method on +`MockHttpServletRequestBuilder`. + [[configuration-snippet-encoding]] diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/CustomUriConfiguration.java index 265e414be..038d5354b 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/CustomUriConfiguration.java @@ -38,8 +38,7 @@ public void setUp() { .apply(documentationConfiguration().uris() .withScheme("https") .withHost("example.com") - .withPort(443) - .withContextPath("/api")) + .withPort(443)) .build(); // end::custom-uri-configuration[] } diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/PathParameters.java index 8bff0a44a..1754947ec 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/PathParameters.java @@ -17,9 +17,9 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.MockMvc; diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 1ca668b8f..132568a23 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -17,13 +17,13 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; diff --git a/docs/src/test/java/com/example/RequestParameters.java b/docs/src/test/java/com/example/RequestParameters.java index 62e882917..9155a5b9f 100644 --- a/docs/src/test/java/com/example/RequestParameters.java +++ b/docs/src/test/java/com/example/RequestParameters.java @@ -17,10 +17,10 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.MockMvc; 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 68d595557..71bca2b5a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -16,10 +16,17 @@ package org.springframework.restdocs; +import static org.springframework.restdocs.util.IterableEnumeration.iterable; + import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.springframework.restdocs.operation.MockMvcOperationRequestFactory; +import org.springframework.restdocs.operation.MockMvcOperationResponseFactory; +import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -44,14 +51,24 @@ public class RestDocumentationResultHandler implements ResultHandler { @Override public void handle(MvcResult result) throws Exception { + Map attributes = new HashMap<>(); + for (String name : iterable(result.getRequest().getAttributeNames())) { + attributes.put(name, result.getRequest().getAttribute(name)); + } + StandardOperation operation = new StandardOperation(this.identifier, + new MockMvcOperationRequestFactory().createOperationRequest(result + .getRequest()), + new MockMvcOperationResponseFactory().createOperationResponse(result + .getResponse()), attributes); for (Snippet snippet : getSnippets(result)) { - snippet.document(this.identifier, result); + snippet.document(operation); } } @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result.getRequest() + List combinedSnippets = new ArrayList<>((List) result + .getRequest() .getAttribute("org.springframework.restdocs.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java index c0fbc03ed..30baa03c2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java @@ -17,7 +17,6 @@ package org.springframework.restdocs.config; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.util.StringUtils; /** * A configurer that can be used to configure the documented URIs @@ -44,20 +43,12 @@ public class UriConfigurer extends AbstractNestedConfigurer document(MvcResult result) throws IOException { + public Map createModel(Operation operation) throws IOException { Map model = new HashMap(); - model.put("arguments", getCurlCommandArguments(result)); + model.put("arguments", getCurlCommandArguments(operation)); return model; } - private String getCurlCommandArguments(MvcResult result) throws IOException { + private String getCurlCommandArguments(Operation operation) throws IOException { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - result.getRequest()); - printer.print("'"); - writeAuthority(request, printer); - writePathAndQueryString(request, printer); + printer.print(operation.getRequest().getUri()); printer.print("'"); writeOptionToIncludeHeadersInOutput(printer); - writeHttpMethodIfNecessary(request, printer); - writeHeaders(request, printer); - - if (request.isMultipartRequest()) { - writeParts(request, printer); - } + writeHttpMethodIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest(), printer); + writePartsIfNecessary(operation.getRequest(), printer); - writeContent(request, printer); + writeContent(operation.getRequest(), printer); return command.toString(); } - private void writeAuthority(DocumentableHttpServletRequest request, PrintWriter writer) { - writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); - - if (isNonStandardPort(request)) { - writer.print(String.format(":%d", request.getPort())); - } - } - - private boolean isNonStandardPort(DocumentableHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); - } - - private void writePathAndQueryString(DocumentableHttpServletRequest request, - PrintWriter writer) { - if (StringUtils.hasText(request.getContextPath())) { - writer.print(String.format(request.getContextPath().startsWith("/") ? "%s" - : "/%s", request.getContextPath())); - } - - writer.print(request.getRequestUriWithQueryString()); - } - private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { writer.print(" -i"); } - private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request, - PrintWriter writer) { - if (!request.isGetRequest()) { + private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { + if (!HttpMethod.GET.equals(request.getMethod())) { writer.print(String.format(" -X %s", request.getMethod())); } } - private void writeHeaders(DocumentableHttpServletRequest request, PrintWriter writer) { + private void writeHeaders(OperationRequest request, PrintWriter writer) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); @@ -127,45 +89,48 @@ private void writeHeaders(DocumentableHttpServletRequest request, PrintWriter wr } } - private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) throws IOException { - for (Entry> entry : request.getMultipartFiles() - .entrySet()) { - for (MultipartFile file : entry.getValue()) { - writer.printf(" -F '%s=", file.getName()); - if (!StringUtils.hasText(file.getOriginalFilename())) { - writer.append(new String(file.getBytes())); - } - else { - writer.printf("@%s", file.getOriginalFilename()); - } - - if (StringUtils.hasText(file.getContentType())) { - writer.append(";type=").append(file.getContentType()); - } - writer.append("'"); + for (OperationRequestPart part : request.getParts()) { + writer.printf(" -F '%s=", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + writer.append(new String(part.getContent())); + } + else { + writer.printf("@%s", part.getSubmittedFileName()); + } + if (part.getHeaders().getContentType() != null) { + writer.append(";type=").append( + part.getHeaders().getContentType().toString()); } - } + writer.append("'"); + } } - private void writeContent(DocumentableHttpServletRequest request, PrintWriter writer) + private void writeContent(OperationRequest request, PrintWriter writer) throws IOException { - if (request.getContentLength() > 0) { - writer.print(String.format(" -d '%s'", request.getContentAsString())); + if (request.getContent().length > 0) { + writer.print(String.format(" -d '%s'", new String(request.getContent()))); } - else if (request.isMultipartRequest()) { - for (Entry entry : request.getParameterMap().entrySet()) { + else if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { for (String value : entry.getValue()) { writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); } } } - else if (request.isPostRequest() || request.isPutRequest()) { - String queryString = request.getParameterMapAsQueryString(); + else if (isPutOrPost(request)) { + String queryString = request.getParameters().toQueryString(); if (StringUtils.hasText(queryString)) { writer.print(String.format(" -d '%s'", queryString)); } } } + + private boolean isPutOrPost(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) + || HttpMethod.POST.equals(request.getMethod()); + } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 7ff397566..fd30849a2 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -26,13 +26,14 @@ import java.util.Map.Entry; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; /** * A {@link Snippet} that documents an HTTP request. @@ -52,27 +53,31 @@ class HttpRequestSnippet extends TemplatedSnippet { } @Override - public Map document(MvcResult result) throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - result.getRequest()); + public Map createModel(Operation operation) throws IOException { Map model = new HashMap(); - model.put("method", result.getRequest().getMethod()); - model.put("path", request.getRequestUriWithQueryString()); - model.put("headers", getHeaders(request)); - model.put("requestBody", getRequestBody(request)); + model.put("method", operation.getRequest().getMethod()); + model.put( + "path", + operation.getRequest().getUri().getRawPath() + + (StringUtils.hasText(operation.getRequest().getUri() + .getRawQuery()) ? "?" + + operation.getRequest().getUri().getRawQuery() + : "")); + model.put("headers", getHeaders(operation.getRequest())); + model.put("requestBody", getRequestBody(operation.getRequest())); return model; } - private List> getHeaders(DocumentableHttpServletRequest request) { + private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); if (requiresHostHeader(request)) { - headers.add(header(HttpHeaders.HOST, request.getHost())); + headers.add(header(HttpHeaders.HOST, request.getUri().getHost())); } for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { if (header.getKey() == HttpHeaders.CONTENT_TYPE - && request.isMultipartRequest()) { + && !request.getParts().isEmpty()) { headers.add(header(header.getKey(), String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); } @@ -82,53 +87,54 @@ private List> getHeaders(DocumentableHttpServletRequest requ } } - if (requiresFormEncodingContentType(request)) { + if (requiresFormEncodingContentTypeHeader(request)) { headers.add(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)); } return headers; } - private String getRequestBody(DocumentableHttpServletRequest request) - throws IOException { + private String getRequestBody(OperationRequest request) throws IOException { StringWriter httpRequest = new StringWriter(); PrintWriter writer = new PrintWriter(httpRequest); - if (request.getContentLength() > 0) { + if (request.getContent().length > 0) { writer.println(); - writer.print(request.getContentAsString()); + writer.print(new String(request.getContent())); } - else if (request.isPostRequest() || request.isPutRequest()) { - if (request.isMultipartRequest()) { - writeParts(request, writer); - } - else { - String queryString = request.getParameterMapAsQueryString(); + else if (isPutOrPost(request)) { + if (request.getParts().isEmpty()) { + String queryString = request.getParameters().toQueryString(); if (StringUtils.hasText(queryString)) { writer.println(); writer.print(queryString); } } + else { + writeParts(request, writer); + } } return httpRequest.toString(); } - private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) + private boolean isPutOrPost(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) + || HttpMethod.POST.equals(request.getMethod()); + } + + private void writeParts(OperationRequest request, PrintWriter writer) throws IOException { writer.println(); - for (Entry parameter : request.getParameterMap().entrySet()) { + for (Entry> parameter : request.getParameters().entrySet()) { for (String value : parameter.getValue()) { writePartBoundary(writer); writePart(parameter.getKey(), value, null, writer); writer.println(); } } - for (Entry> entry : request.getMultipartFiles() - .entrySet()) { - for (MultipartFile file : entry.getValue()) { - writePartBoundary(writer); - writePart(file, writer); - writer.println(); - } + for (OperationRequestPart part : request.getParts()) { + writePartBoundary(writer); + writePart(part, writer); + writer.println(); } writeMultipartEnd(writer); } @@ -137,33 +143,33 @@ private void writePartBoundary(PrintWriter writer) { writer.printf("--%s%n", MULTIPART_BOUNDARY); } - private void writePart(String name, String value, String contentType, + private void writePart(OperationRequestPart part, PrintWriter writer) + throws IOException { + writePart(part.getName(), new String(part.getContent()), part.getHeaders() + .getContentType(), writer); + } + + private void writePart(String name, String value, MediaType contentType, PrintWriter writer) { writer.printf("Content-Disposition: form-data; name=%s%n", name); - if (StringUtils.hasText(contentType)) { + if (contentType != null) { writer.printf("Content-Type: %s%n", contentType); } writer.println(); writer.print(value); } - private void writePart(MultipartFile part, PrintWriter writer) throws IOException { - writePart(part.getName(), new String(part.getBytes()), part.getContentType(), - writer); - } - private void writeMultipartEnd(PrintWriter writer) { writer.printf("--%s--", MULTIPART_BOUNDARY); } - private boolean requiresHostHeader(DocumentableHttpServletRequest request) { + private boolean requiresHostHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.HOST) == null; } - private boolean requiresFormEncodingContentType(DocumentableHttpServletRequest request) { - return request.getHeaders().getContentType() == null - && (request.isPostRequest() || request.isPutRequest()) - && StringUtils.hasText(request.getParameterMapAsQueryString()); + private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { + return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null + && isPutOrPost(request) && !request.getParameters().isEmpty(); } private Map header(String name, String value) { @@ -172,4 +178,5 @@ private Map header(String name, String value) { header.put("value", value); return header; } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index b3dfb3c24..7f4f6b658 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -21,12 +21,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.StringUtils; /** * A {@link Snippet} that documents an HTTP response. @@ -44,24 +45,26 @@ class HttpResponseSnippet extends TemplatedSnippet { } @Override - public Map document(MvcResult result) throws IOException { - HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); + public Map createModel(Operation operation) throws IOException { + OperationResponse response = operation.getResponse(); + HttpStatus status = response.getStatus(); Map model = new HashMap(); model.put( "responseBody", - StringUtils.hasLength(result.getResponse().getContentAsString()) ? String - .format("%n%s", result.getResponse().getContentAsString()) : ""); + response.getContent().length > 0 ? String.format("%n%s", new String( + response.getContent())) : ""); model.put("statusCode", status.value()); model.put("statusReason", status.getReasonPhrase()); - model.put("headers", headers(result)); + model.put("headers", headers(response)); return model; } - private List> headers(MvcResult result) { + private List> headers(OperationResponse response) { List> headers = new ArrayList<>(); - for (String headerName : result.getResponse().getHeaderNames()) { - for (String header : result.getResponse().getHeaders(headerName)) { - headers.add(header(headerName, header)); + for (Entry> header : response.getHeaders().entrySet()) { + List values = header.getValue(); + for (String value : values) { + headers.add(header(header.getKey(), value)); } } return headers; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index 45c73c234..ed877ab70 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.Map; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationResponse; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,10 +35,10 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor { @Override @SuppressWarnings("unchecked") - public Map> extractLinks(MockHttpServletResponse response) + public Map> extractLinks(OperationResponse response) throws IOException { Map jsonContent = this.objectMapper.readValue( - response.getContentAsString(), Map.class); + new String(response.getContent()), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index c114d138e..1c31ad3a9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -7,8 +7,7 @@ import java.util.Map.Entry; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.util.StringUtils; +import org.springframework.restdocs.operation.OperationResponse; /** * {@link LinkExtractor} that delegates to other link extractors based on the response's @@ -31,9 +30,9 @@ class ContentTypeLinkExtractor implements LinkExtractor { } @Override - public Map> extractLinks(MockHttpServletResponse response) + public Map> extractLinks(OperationResponse response) throws IOException { - String contentType = response.getContentType(); + MediaType contentType = response.getHeaders().getContentType(); LinkExtractor extractorForContentType = getExtractorForContentType(contentType); if (extractorForContentType != null) { return extractorForContentType.extractLinks(response); @@ -43,11 +42,10 @@ public Map> extractLinks(MockHttpServletResponse response) + "content type " + contentType); } - private LinkExtractor getExtractorForContentType(String contentType) { - if (StringUtils.hasText(contentType)) { - MediaType mediaType = MediaType.parseMediaType(contentType); + private LinkExtractor getExtractorForContentType(MediaType contentType) { + if (contentType != null) { for (Entry entry : this.linkExtractors.entrySet()) { - if (mediaType.isCompatibleWith(entry.getKey())) { + if (contentType.isCompatibleWith(entry.getKey())) { return entry.getValue(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java index 0627a306c..747731d6d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.Map; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationResponse; /** * A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The @@ -32,14 +32,13 @@ public interface LinkExtractor { /** - * Extract the links from the given response, returning a {@code Map} of links where - * the keys are the link rels. + * Extract the links from the given {@code response}, returning a {@code Map} of links + * where the keys are the link rels. * * @param response The response from which the links are to be extracted * @return The extracted links, keyed by rel * @throws IOException if link extraction fails */ - Map> extractLinks(MockHttpServletResponse response) - throws IOException; + Map> extractLinks(OperationResponse response) throws IOException; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 3557581b7..1e01716c6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -26,10 +26,11 @@ import java.util.Map.Entry; import java.util.Set; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; /** @@ -64,8 +65,9 @@ class LinksSnippet extends TemplatedSnippet { } @Override - protected Map document(MvcResult result) throws IOException { - validate(this.linkExtractor.extractLinks(result.getResponse())); + protected Map createModel(Operation operation) throws IOException { + OperationResponse response = operation.getResponse(); + validate(this.linkExtractor.extractLinks(response)); Map model = new HashMap<>(); model.put("links", createLinksModel()); return model; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java new file mode 100644 index 000000000..60c82404f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java @@ -0,0 +1,162 @@ +/* + * 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.operation; + +import static org.springframework.restdocs.util.IterableEnumeration.iterable; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import javax.servlet.ServletException; +import javax.servlet.http.Part; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * A factory for creating an {@link OperationRequest} from a + * {@link MockHttpServletRequest}. + * + * @author Andy Wilkinson + * + */ +public class MockMvcOperationRequestFactory { + + private static final String SCHEME_HTTP = "http"; + + private static final String SCHEME_HTTPS = "https"; + + private static final int STANDARD_PORT_HTTP = 80; + + private static final int STANDARD_PORT_HTTPS = 443; + + /** + * Creates a new {@code OperationRequest} derived from the given {@code mockRequest}. + * + * @param mockRequest the request + * @return the {@code OperationRequest} + * @throws Exception if the request could not be created + */ + public OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) + throws Exception { + HttpHeaders headers = extractHeaders(mockRequest); + Parameters parameters = extractParameters(mockRequest); + List parts = extractParts(mockRequest); + String queryString = mockRequest.getQueryString(); + if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { + queryString = parameters.toQueryString(); + } + return new StandardOperationRequest(URI.create(getRequestUri(mockRequest) + + (StringUtils.hasText(queryString) ? "?" + queryString : "")), + HttpMethod.valueOf(mockRequest.getMethod()), + FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, + parameters, parts); + } + + private List extractParts(MockHttpServletRequest servletRequest) + throws IOException, ServletException { + List parts = new ArrayList<>(); + for (Part part : servletRequest.getParts()) { + HttpHeaders partHeaders = extractHeaders(part); + List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); + if (part.getContentType() != null && contentTypeHeader == null) { + partHeaders + .setContentType(MediaType.parseMediaType(part.getContentType())); + } + parts.add(new StandardOperationRequestPart(part.getName(), StringUtils + .hasText(part.getSubmittedFileName()) ? part.getSubmittedFileName() + : null, FileCopyUtils.copyToByteArray(part.getInputStream()), + partHeaders)); + } + if (servletRequest instanceof MockMultipartHttpServletRequest) { + for (Entry> entry : ((MockMultipartHttpServletRequest) servletRequest) + .getMultiFileMap().entrySet()) { + for (MultipartFile file : entry.getValue()) { + HttpHeaders partHeaders = new HttpHeaders(); + if (StringUtils.hasText(file.getContentType())) { + partHeaders.setContentType(MediaType.parseMediaType(file + .getContentType())); + } + parts.add(new StandardOperationRequestPart(file.getName(), + StringUtils.hasText(file.getOriginalFilename()) ? file + .getOriginalFilename() : null, file.getBytes(), + partHeaders)); + } + } + } + return parts; + } + + private HttpHeaders extractHeaders(Part part) { + HttpHeaders partHeaders = new HttpHeaders(); + for (String headerName : part.getHeaderNames()) { + for (String value : part.getHeaders(headerName)) { + partHeaders.add(headerName, value); + } + } + return partHeaders; + } + + private Parameters extractParameters(MockHttpServletRequest servletRequest) { + Parameters parameters = new Parameters(); + for (String name : iterable(servletRequest.getParameterNames())) { + for (String value : servletRequest.getParameterValues(name)) { + parameters.add(name, value); + } + } + return parameters; + } + + private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { + HttpHeaders headers = new HttpHeaders(); + for (String headerName : iterable(servletRequest.getHeaderNames())) { + for (String value : iterable(servletRequest.getHeaders(headerName))) { + headers.add(headerName, value); + } + } + return headers; + } + + private boolean isNonStandardPort(MockHttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTPS); + } + + private String getRequestUri(MockHttpServletRequest request) { + StringWriter uriWriter = new StringWriter(); + PrintWriter printer = new PrintWriter(uriWriter); + + printer.printf("%s://%s", request.getScheme(), request.getServerName()); + if (isNonStandardPort(request)) { + printer.printf(":%d", request.getServerPort()); + } + printer.print(request.getRequestURI()); + return uriWriter.toString(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java new file mode 100644 index 000000000..5ace0f069 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java @@ -0,0 +1,52 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * A factory for creating an {@link OperationResponse} derived from a + * {@link MockHttpServletResponse}. + * + * @author Andy Wilkinson + */ +public class MockMvcOperationResponseFactory { + + /** + * Create a new {@code OperationResponse} derived from the given {@code mockResponse}. + * + * @param mockResponse the response + * @return the {@code OperationResponse} + */ + public OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { + return new StandardOperationResponse( + HttpStatus.valueOf(mockResponse.getStatus()), + extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); + } + + private HttpHeaders extractHeaders(MockHttpServletResponse response) { + HttpHeaders headers = new HttpHeaders(); + for (String headerName : response.getHeaderNames()) { + for (String value : response.getHeaders(headerName)) { + headers.add(headerName, value); + } + } + return headers; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java new file mode 100644 index 000000000..fdd5fe116 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java @@ -0,0 +1,56 @@ +/* + * 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.operation; + +import java.util.Map; + +/** + * Describes an operation performed on a RESTful service. + * + * @author Andy Wilkinson + */ +public interface Operation { + + /** + * Returns a {@code Map} of attributes associated with the operation. + * + * @return the attributes + */ + Map getAttributes(); + + /** + * Returns the name of the operation. + * + * @return the name + */ + String getName(); + + /** + * Returns the request that was sent. + * + * @return the request + */ + OperationRequest getRequest(); + + /** + * Returns the response that was received. + * + * @return the response + */ + OperationResponse getResponse(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java new file mode 100644 index 000000000..919e84adb --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -0,0 +1,79 @@ +/* + * 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.operation; + +import java.net.URI; +import java.util.Collection; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * The request that was sent as part of performing an operation on a RESTful service. + * + * @author Andy Wilkinson + * @see Operation#getRequest() + */ +public interface OperationRequest { + + /** + * Returns the contents of the request. If the request has no content an empty array + * is returned + * + * @return the contents, never {@code null} + */ + byte[] getContent(); + + /** + * Returns the headers that were included in the request. + * + * @return the headers + */ + HttpHeaders getHeaders(); + + /** + * Returns the HTTP method of the request + * + * @return the HTTP method + */ + HttpMethod getMethod(); + + /** + * Returns the request's parameters. For a {@code GET} request, the parameters are + * derived from the query string. For a {@code POST} request, the parameters are + * derived form the request's body. + * + * @return the parameters + */ + Parameters getParameters(); + + /** + * Returns the request's parts, provided that it is a multipart request. If not, then + * an empty {@link Collection} is returned. + * + * @return the parts + */ + Collection getParts(); + + /** + * Returns the request's URI. + * + * @return the URI + */ + URI getUri(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java new file mode 100644 index 000000000..09e9bb80f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java @@ -0,0 +1,57 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * A part of a multipart request + * + * @author awilkinson + * @see OperationRequest#getParts() + */ +public interface OperationRequestPart { + + /** + * Returns the name of the part. + * + * @return the name + */ + String getName(); + + /** + * Returns the name of the file that is being uploaded in this part. + * + * @return the name of the file + */ + String getSubmittedFileName(); + + /** + * Returns the contents of the part. + * + * @return the contents + */ + byte[] getContent(); + + /** + * Returns the part's headers. + * + * @return the headers + */ + HttpHeaders getHeaders(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java new file mode 100644 index 000000000..d72544773 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -0,0 +1,52 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * The response that was received as part of performing an operation on a RESTful service. + * + * @author Andy Wilkinson + * @see Operation + * @see Operation#getRequest() + */ +public interface OperationResponse { + + /** + * Returns the status of the response. + * + * @return the status + */ + HttpStatus getStatus(); + + /** + * Returns the headers in the response. + * + * @return the headers + */ + HttpHeaders getHeaders(); + + /** + * Returns the contents of the response. If the response has no content an empty array + * is returned. + * + * @return the contents, never {@code null} + */ + byte[] getContent(); +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java new file mode 100644 index 000000000..d53392d1d --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -0,0 +1,64 @@ +/* + * 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.operation; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Map; + +import org.springframework.util.LinkedMultiValueMap; + +/** + * The parameters received in a request + * + * @author Andy Wilkinson + */ +@SuppressWarnings("serial") +public class Parameters extends LinkedMultiValueMap { + + /** + * Converts the parameters to a query string suitable for use in a URI or the body of + * a form-encoded request + * + * @return the query string + */ + public String toQueryString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry> entry : entrySet()) { + for (String value : entry.getValue()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(urlEncodeUTF8(entry.getKey())).append('=') + .append(urlEncodeUTF8(value)); + } + } + return sb.toString(); + } + + private static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", + ex); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java new file mode 100644 index 000000000..a748df456 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java @@ -0,0 +1,72 @@ +/* + * 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.operation; + +import java.util.Map; + +/** + * Standard implementation of {@link Operation} + * + * @author Andy Wilkinson + */ +public class StandardOperation implements Operation { + + private final String name; + + private final OperationRequest request; + + private final OperationResponse response; + + private final Map attributes; + + /** + * Creates a new {@code StandardOperation} + * + * @param name the name of the operation + * @param request the request that was sent + * @param response the response that was received + * @param attributes attributes to associate with the operation + */ + public StandardOperation(String name, OperationRequest request, + OperationResponse response, Map attributes) { + this.name = name; + this.request = request; + this.response = response; + this.attributes = attributes; + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OperationRequest getRequest() { + return this.request; + } + + @Override + public OperationResponse getResponse() { + return this.response; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java new file mode 100644 index 000000000..37ed936f7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -0,0 +1,98 @@ +/* + * 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.operation; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * Standard implementation of {@link OperationRequest}. + * + * @author Andy Wilkinson + */ +public class StandardOperationRequest implements OperationRequest { + + private byte[] content; + + private HttpHeaders headers; + + private HttpMethod method; + + private Parameters parameters; + + private Collection parts; + + private URI uri; + + /** + * Creates a new request with the given {@code uri} and {@code method}. The request + * will have the given {@code headers}, {@code parameters}, and {@code parts}. + * + * @param uri the uri + * @param method the method + * @param content the content + * @param headers the headers + * @param parameters the parameters + * @param parts the parts + */ + public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, + HttpHeaders headers, Parameters parameters, + Collection parts) { + this.uri = uri; + this.method = method; + this.content = content == null ? new byte[0] : content; + this.headers = headers; + this.parameters = parameters; + this.parts = parts; + } + + @Override + public byte[] getContent() { + return Arrays.copyOf(this.content, this.content.length); + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public Parameters getParameters() { + return this.parameters; + } + + @Override + public Collection getParts() { + return Collections.unmodifiableCollection(this.parts); + } + + @Override + public URI getUri() { + return this.uri; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java new file mode 100644 index 000000000..cfa93d117 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -0,0 +1,72 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * Standard implementation of {@code OperationRequestPart}. + * + * @author Andy Wilkinson + */ +public class StandardOperationRequestPart implements OperationRequestPart { + + private final String name; + + private final String submittedFileName; + + private final byte[] content; + + private final HttpHeaders headers; + + /** + * Creates a new {@code StandardOperationRequestPart} with the given {@code name}. + * + * @param name the name of the part + * @param submittedFileName the name of the file being uploaded by this part + * @param content the contents of the part + * @param headers the headers of the part + */ + public StandardOperationRequestPart(String name, String submittedFileName, + byte[] content, HttpHeaders headers) { + this.name = name; + this.submittedFileName = submittedFileName; + this.content = content; + this.headers = headers; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getSubmittedFileName() { + return this.submittedFileName; + } + + @Override + public byte[] getContent() { + return this.content; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java new file mode 100644 index 000000000..f7a1c5948 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -0,0 +1,65 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * Standard implementation of {@link OperationResponse}. + * + * @author Andy Wilkinson + */ +public class StandardOperationResponse implements OperationResponse { + + private final HttpStatus status; + + private final HttpHeaders headers; + + private final byte[] content; + + /** + * Creates a new response with the given {@code status}, {@code headers}, and + * {@code content}. + * + * @param status the status of the response + * @param headers the headers of the response + * @param content the content of the response + */ + public StandardOperationResponse(HttpStatus status, HttpHeaders headers, + byte[] content) { + this.status = status; + this.headers = headers; + this.content = content == null ? new byte[0] : content; + } + + @Override + public HttpStatus getStatus() { + return this.status; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public byte[] getContent() { + return this.content; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index b8d4aa2dd..ec93b8d1f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -17,19 +17,16 @@ package org.springframework.restdocs.payload; import java.io.IOException; -import java.io.Reader; -import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** @@ -54,22 +51,22 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { } @Override - protected Map document(MvcResult result) throws IOException { - MediaType contentType = getContentType(result); - PayloadHandler payloadHandler; + protected Map createModel(Operation operation) throws IOException { + MediaType contentType = getContentType(operation); + ContentHandler contentHandler; if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - payloadHandler = new XmlPayloadHandler(readPayload(result)); + contentHandler = new XmlContentHandler(getContent(operation)); } else { - payloadHandler = new JsonPayloadHandler(readPayload(result)); + contentHandler = new JsonContentHandler(getContent(operation)); } - validateFieldDocumentation(payloadHandler); + validateFieldDocumentation(contentHandler); for (FieldDescriptor descriptor : this.fieldDescriptors) { if (descriptor.getType() == null) { - descriptor.type(payloadHandler.determineFieldType(descriptor.getPath())); + descriptor.type(contentHandler.determineFieldType(descriptor.getPath())); } } @@ -82,17 +79,11 @@ protected Map document(MvcResult result) throws IOException { return model; } - private String readPayload(MvcResult result) throws IOException { - StringWriter writer = new StringWriter(); - FileCopyUtils.copy(getPayloadReader(result), writer); - return writer.toString(); - } - - private void validateFieldDocumentation(PayloadHandler payloadHandler) { + private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); String undocumentedPayload = payloadHandler - .getUndocumentedPayload(this.fieldDescriptors); + .getUndocumentedContent(this.fieldDescriptors); if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { String message = ""; @@ -115,8 +106,8 @@ private void validateFieldDocumentation(PayloadHandler payloadHandler) { } } - protected abstract MediaType getContentType(MvcResult result); + protected abstract MediaType getContentType(Operation operation); - protected abstract Reader getPayloadReader(MvcResult result) throws IOException; + protected abstract byte[] getContent(Operation operation) throws IOException; } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java new file mode 100644 index 000000000..0104f143a --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -0,0 +1,61 @@ +/* + * 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.List; + +/** + * A handler for the content of a request or response + * + * @author Andy Wilkinson + */ +interface ContentHandler { + + /** + * Finds the fields that are missing from the handler's payload. A field is missing if + * it is described by one of the {@code fieldDescriptors} but is not present in the + * payload. + * + * @param fieldDescriptors the descriptors + * @return descriptors for the fields that are missing from the payload + * @throws PayloadHandlingException if a failure occurs + */ + List findMissingFields(List fieldDescriptors); + + /** + * Returns modified content, formatted as a String, that only contains the fields that + * are undocumented. A field is undocumented if it is present in the handler's content + * but is not described by the given {@code fieldDescriptors}. If the content is + * completely documented, {@code null} is returned + * + * @param fieldDescriptors the descriptors + * @return the undocumented content, or {@code null} if all of the content is + * documented + * @throws PayloadHandlingException if a failure occurs + */ + String getUndocumentedContent(List fieldDescriptors); + + /** + * Returns the type of the field with the given {@code path} based on the content of + * the payload. + * + * @param path the field path + * @return the type of the field + */ + Object determineFieldType(String path); + +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java similarity index 77% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 93c824cbf..2fe9e8972 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -10,27 +10,27 @@ import com.fasterxml.jackson.databind.SerializationFeature; /** - * A {@link PayloadHandler} for JSON payloads + * A {@link ContentHandler} for JSON content * * @author Andy Wilkinson */ -class JsonPayloadHandler implements PayloadHandler { +class JsonContentHandler implements ContentHandler { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); private final ObjectMapper objectMapper = new ObjectMapper() .enable(SerializationFeature.INDENT_OUTPUT); - private final String rawPayload; + private final byte[] rawContent; - JsonPayloadHandler(String payload) throws IOException { - this.rawPayload = payload; + JsonContentHandler(byte[] content) throws IOException { + this.rawContent = content; } @Override public List findMissingFields(List fieldDescriptors) { List missingFields = new ArrayList(); - Object payload = readPayload(); + Object payload = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField( @@ -43,15 +43,15 @@ public List findMissingFields(List fieldDescri } @Override - public String getUndocumentedPayload(List fieldDescriptors) { - Object payload = readPayload(); + public String getUndocumentedContent(List fieldDescriptors) { + Object content = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, payload); + this.fieldProcessor.remove(path, content); } - if (!isEmpty(payload)) { + if (!isEmpty(content)) { try { - return this.objectMapper.writeValueAsString(payload); + return this.objectMapper.writeValueAsString(content); } catch (JsonProcessingException ex) { throw new PayloadHandlingException(ex); @@ -60,9 +60,9 @@ public String getUndocumentedPayload(List fieldDescriptors) { return null; } - private Object readPayload() { + private Object readContent() { try { - return new ObjectMapper().readValue(this.rawPayload, Object.class); + return new ObjectMapper().readValue(this.rawContent, Object.class); } catch (IOException ex) { throw new PayloadHandlingException(ex); @@ -79,7 +79,7 @@ private boolean isEmpty(Object object) { @Override public Object determineFieldType(String path) { try { - return new JsonFieldTypeResolver().resolveFieldType(path, readPayload()); + return new JsonFieldTypeResolver().resolveFieldType(path, readContent()); } catch (FieldDoesNotExistException ex) { String message = "Cannot determine the type of the field '" + path + "' as" diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java deleted file mode 100644 index 5d6c2d479..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.springframework.restdocs.payload; - -import java.util.List; - -/** - * A handler for a request or response payload - * - * @author Andy Wilkinson - */ -interface PayloadHandler { - - /** - * Finds the fields that are missing from the handler's payload. A field is missing if - * it is described by one of the {@code fieldDescriptors} but is not present in the - * payload. - * - * @param fieldDescriptors the descriptors - * @return descriptors for the fields that are missing from the payload - * @throws PayloadHandlingException if a failure occurs - */ - List findMissingFields(List fieldDescriptors); - - /** - * Returns a modified payload, formatted as a String, that only contains the fields - * that are undocumented. A field is undocumented if it is present in the handler's - * payload but is not described by the given {@code fieldDescriptors}. If the payload - * is completely documented, {@code null} is returned - * - * @param fieldDescriptors the descriptors - * @return the undocumented payload, or {@code null} if all of the payload is - * documented - * @throws PayloadHandlingException if a failure occurs - */ - String getUndocumentedPayload(List fieldDescriptors); - - /** - * Returns the type of the field with the given {@code path} based on the content of - * the payload. - * - * @param path the field path - * @return the type of the field - */ - Object determineFieldType(String path); - -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index c7e25c9b2..1f72d4a30 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -13,16 +13,16 @@ * 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 java.util.Map; import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.test.web.servlet.MvcResult; /** * A {@link Snippet} that documents the fields in a request. @@ -40,17 +40,13 @@ class RequestFieldsSnippet extends AbstractFieldsSnippet { } @Override - protected Reader getPayloadReader(MvcResult result) throws IOException { - return result.getRequest().getReader(); + protected MediaType getContentType(Operation operation) { + return operation.getRequest().getHeaders().getContentType(); } @Override - protected MediaType getContentType(MvcResult result) { - String contentType = result.getRequest().getContentType(); - if (contentType != null) { - return MediaType.valueOf(contentType); - } - return null; + protected byte[] getContent(Operation operation) throws IOException { + return operation.getRequest().getContent(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 440e5b719..4cd92ceaf 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -16,14 +16,12 @@ package org.springframework.restdocs.payload; import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; import java.util.List; import java.util.Map; import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.test.web.servlet.MvcResult; /** * A {@link Snippet} that documents the fields in a response. @@ -42,17 +40,13 @@ class ResponseFieldsSnippet extends AbstractFieldsSnippet { } @Override - protected Reader getPayloadReader(MvcResult result) throws IOException { - return new StringReader(result.getResponse().getContentAsString()); + protected MediaType getContentType(Operation operation) { + return operation.getResponse().getHeaders().getContentType(); } @Override - protected MediaType getContentType(MvcResult result) { - String contentType = result.getResponse().getContentType(); - if (contentType != null) { - return MediaType.valueOf(contentType); - } - return null; + protected byte[] getContent(Operation operation) throws IOException { + return operation.getResponse().getContent(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java similarity index 79% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index c50aaba03..53eb22e60 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -1,6 +1,22 @@ +/* + * 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.StringReader; +import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; @@ -24,17 +40,17 @@ import org.xml.sax.InputSource; /** - * A {@link PayloadHandler} for XML payloads + * A {@link ContentHandler} for XML content * * @author Andy Wilkinson */ -class XmlPayloadHandler implements PayloadHandler { +class XmlContentHandler implements ContentHandler { private final DocumentBuilder documentBuilder; - private final String rawPayload; + private final byte[] rawContent; - XmlPayloadHandler(String rawPayload) { + XmlContentHandler(byte[] rawContent) { try { this.documentBuilder = DocumentBuilderFactory.newInstance() .newDocumentBuilder(); @@ -42,7 +58,7 @@ class XmlPayloadHandler implements PayloadHandler { catch (ParserConfigurationException ex) { throw new IllegalStateException("Failed to create document builder", ex); } - this.rawPayload = rawPayload; + this.rawContent = rawContent; } @Override @@ -73,8 +89,8 @@ private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document pay private Document readPayload() { try { - return this.documentBuilder.parse(new InputSource(new StringReader( - this.rawPayload))); + return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream( + this.rawContent))); } catch (Exception ex) { throw new PayloadHandlingException(ex); @@ -86,7 +102,7 @@ private XPathExpression createXPath(String fieldPath) throws XPathExpressionExce } @Override - public String getUndocumentedPayload(List fieldDescriptors) { + public String getUndocumentedContent(List fieldDescriptors) { Document payload = readPayload(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { NodeList matchingNodes; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 35a3ba2a1..df0411dc4 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -1,3 +1,19 @@ +/* + * 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.request; import java.io.IOException; @@ -10,12 +26,11 @@ import java.util.Map.Entry; import java.util.Set; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; -abstract class AbstractParametersSnippet extends - TemplatedSnippet { +abstract class AbstractParametersSnippet extends TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); @@ -30,8 +45,8 @@ protected AbstractParametersSnippet(String snippetName, } @Override - protected Map document(MvcResult result) throws IOException { - verifyParameterDescriptors(result); + protected Map createModel(Operation operation) throws IOException { + verifyParameterDescriptors(operation); Map model = new HashMap<>(); List> parameters = new ArrayList<>(); @@ -42,8 +57,8 @@ protected Map document(MvcResult result) throws IOException { return model; } - protected void verifyParameterDescriptors(MvcResult result) { - Set actualParameters = extractActualParameters(result); + protected void verifyParameterDescriptors(Operation operation) { + Set actualParameters = extractActualParameters(operation); Set expectedParameters = this.descriptorsByName.keySet(); Set undocumentedParameters = new HashSet(actualParameters); undocumentedParameters.removeAll(expectedParameters); @@ -58,7 +73,7 @@ protected void verifyParameterDescriptors(MvcResult result) { } } - protected abstract Set extractActualParameters(MvcResult result); + protected abstract Set extractActualParameters(Operation operation); protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index a8ae9d2af..99c3c7658 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -24,9 +24,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; /** @@ -48,9 +48,9 @@ class PathParametersSnippet extends AbstractParametersSnippet { } @Override - protected Map document(MvcResult result) throws IOException { - Map model = super.document(result); - model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(result))); + protected Map createModel(Operation operation) throws IOException { + Map model = super.createModel(operation); + model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation))); return model; } @@ -63,8 +63,8 @@ private String remoteQueryStringIfPresent(String urlTemplate) { } @Override - protected Set extractActualParameters(MvcResult result) { - String urlTemplate = extractUrlTemplate(result); + protected Set extractActualParameters(Operation operation) { + String urlTemplate = extractUrlTemplate(operation); Matcher matcher = NAMES_PATTERN.matcher(urlTemplate); Set actualParameters = new HashSet<>(); while (matcher.find()) { @@ -74,8 +74,8 @@ protected Set extractActualParameters(MvcResult result) { return actualParameters; } - private String extractUrlTemplate(MvcResult result) { - String urlTemplate = (String) result.getRequest().getAttribute( + private String extractUrlTemplate(Operation operation) { + String urlTemplate = (String) operation.getAttributes().get( "org.springframework.restdocs.urlTemplate"); Assert.notNull(urlTemplate, "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 1916c9b69..386fcd1a6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -22,9 +22,9 @@ import javax.servlet.ServletRequest; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.bind.annotation.RequestParam; /** @@ -67,8 +67,8 @@ protected void verificationFailed(Set undocumentedParameters, } @Override - protected Set extractActualParameters(MvcResult result) { - return result.getRequest().getParameterMap().keySet(); + protected Set extractActualParameters(Operation operation) { + return operation.getRequest().getParameters().keySet(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java index 44d78999c..52640b3f4 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -81,9 +81,7 @@ public Object intercept(Object proxy, Method method, Object[] args, return this.modifiedContent; } if (this.getContentAsByteArray.equals(method)) { - throw new UnsupportedOperationException( - "Following modification, the response's content should be" - + " accessed as a String"); + return this.modifiedContent.getBytes(); } return method.invoke(this.delegate, args); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java deleted file mode 100644 index 7c2433999..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java +++ /dev/null @@ -1,276 +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.snippet; - -import static org.springframework.restdocs.util.IterableEnumeration.iterable; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockMultipartHttpServletRequest; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.multipart.MultipartFile; - -/** - * An {@link HttpServletRequest} wrapper that provides a limited set of methods intended - * to help in the documentation of the request. - * - * @author Andy Wilkinson - * @author Jonathan Pearlin - */ -public class DocumentableHttpServletRequest { - - private final MockHttpServletRequest delegate; - - /** - * Creates a new {@link DocumentableHttpServletRequest} to document the given - * {@code request}. - * - * @param request the request that is to be documented - */ - public DocumentableHttpServletRequest(MockHttpServletRequest request) { - this.delegate = request; - } - - /** - * Whether or not this request is a {@code GET} request. - * - * @return {@code true} if it is a {@code GET} request, otherwise {@code false} - * @see HttpServletRequest#getMethod() - */ - public boolean isGetRequest() { - return RequestMethod.GET == RequestMethod.valueOf(this.delegate.getMethod()); - } - - /** - * Whether or not this request is a {@code POST} request. - * - * @return {@code true} if it is a {@code POST} request, otherwise {@code false} - * @see HttpServletRequest#getMethod() - */ - public boolean isPostRequest() { - return RequestMethod.POST == RequestMethod.valueOf(this.delegate.getMethod()); - } - - /** - * Whether or not this request is a {@code PUT} request. - * - * @return {@code true} if it is a {@code PUT} request, otherwise {@code false} - * @see HttpServletRequest#getMethod() - */ - public boolean isPutRequest() { - return RequestMethod.PUT == RequestMethod.valueOf(this.delegate.getMethod()); - } - - /** - * Whether or not this is a multipart request. - * - * @return {@code true} if it is a multipart request, otherwise {@code false}. - * @see MockMultipartHttpServletRequest - */ - public boolean isMultipartRequest() { - return this.delegate instanceof MockMultipartHttpServletRequest; - } - - /** - * Returns a {@code Map} of the request's multipart files, or {@code null} if this - * request is not a multipart request. - * - * @return a {@code Map} of the multipart files contained in the request, or - * {@code null} - * @see #isMultipartRequest() - */ - public MultiValueMap getMultipartFiles() { - if (!isMultipartRequest()) { - return null; - } - return ((MockMultipartHttpServletRequest) this.delegate).getMultiFileMap(); - } - - /** - * Returns the request's headers. The headers are ordered based on the ordering of - * {@link HttpServletRequest#getHeaderNames()} and - * {@link HttpServletRequest#getHeaders(String)}. - * - * @return the request's headers - * @see HttpServletRequest#getHeaderNames() - * @see HttpServletRequest#getHeaders(String) - */ - public HttpHeaders getHeaders() { - HttpHeaders httpHeaders = new HttpHeaders(); - for (String headerName : iterable(this.delegate.getHeaderNames())) { - for (String header : iterable(this.delegate.getHeaders(headerName))) { - httpHeaders.add(headerName, header); - } - } - return httpHeaders; - } - - /** - * Returns the request's scheme. - * - * @return the request's scheme - * @see HttpServletRequest#getScheme() - */ - public String getScheme() { - return this.delegate.getScheme(); - } - - /** - * Returns the name of the host to which the request was sent. - * - * @return the host's name - * @see HttpServletRequest#getServerName() - */ - public String getHost() { - return this.delegate.getServerName(); - } - - /** - * Returns the port to which the request was sent. - * - * @return the port - * @see HttpServletRequest#getServerPort() - */ - public int getPort() { - return this.delegate.getServerPort(); - } - - /** - * Returns the request's method. - * - * @return the request's method - * @see HttpServletRequest#getMethod() - */ - public String getMethod() { - return this.delegate.getMethod(); - } - - /** - * Returns the length of the request's content - * - * @return the content length - * @see HttpServletRequest#getContentLength() - */ - public long getContentLength() { - return this.delegate.getContentLengthLong(); - } - - /** - * Returns a {@code String} of the request's content - * - * @return the request's content - * @throws IOException if the content cannot be read - */ - public String getContentAsString() throws IOException { - StringWriter bodyWriter = new StringWriter(); - FileCopyUtils.copy(this.delegate.getReader(), bodyWriter); - return bodyWriter.toString(); - } - - /** - * Returns the request's URI including its query string. The query string is - * determined by calling {@link HttpServletRequest#getQueryString()}. If it's - * {@code null} and it is a {@code GET} request, the query string is then constructed - * from the request's {@link HttpServletRequest#getParameterMap()} parameter map. - * - * @return the URI of the request, including its query string - */ - public String getRequestUriWithQueryString() { - StringBuilder sb = new StringBuilder(); - sb.append(this.delegate.getRequestURI()); - String queryString = getQueryString(); - if (StringUtils.hasText(queryString)) { - sb.append('?').append(queryString); - } - return sb.toString(); - } - - /** - * Returns the request's parameter map formatted as a query string - * - * @return The query string derived from the request's parameter map - * @see HttpServletRequest#getParameterMap() - */ - public String getParameterMapAsQueryString() { - return toQueryString(this.delegate.getParameterMap()); - } - - /** - * Returns the request's context path - * - * @return The context path of the request - * @see HttpServletRequest#getContextPath() - */ - public String getContextPath() { - return this.delegate.getContextPath(); - } - - /** - * Returns a map of the request's parameters - * @return The map of parameters - * @see HttpServletRequest#getParameterMap() - */ - public Map getParameterMap() { - return this.delegate.getParameterMap(); - } - - private String getQueryString() { - if (this.delegate.getQueryString() != null) { - return this.delegate.getQueryString(); - } - if (isGetRequest()) { - return getParameterMapAsQueryString(); - } - return null; - } - - private static String toQueryString(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - for (String value : entry.getValue()) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append(urlEncodeUTF8(entry.getKey())).append('=') - .append(urlEncodeUTF8(value)); - } - } - return sb.toString(); - } - - private static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", - ex); - } - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java index 323af9e2b..63438924d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java @@ -18,7 +18,7 @@ import java.io.IOException; -import org.springframework.test.web.servlet.MvcResult; +import org.springframework.restdocs.operation.Operation; /** * A {@link Snippet} is used to document aspects of a call to a RESTful API. @@ -28,13 +28,11 @@ public interface Snippet { /** - * Documents the call to the RESTful API described by the given {@code result}. The - * call is identified by the given {@code operation}. + * Documents the call to the RESTful API described by the given {@code operation}. * * @param operation the API operation - * @param result the result of the operation - * @throws IOException if a failure occurs will documenting the result + * @throws IOException if a failure occurs will documenting the operation */ - void document(String operation, MvcResult result) throws IOException; + void document(Operation operation) throws IOException; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index de9f966a2..7fc188bce 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -21,9 +21,9 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.templates.Template; import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.test.web.servlet.MvcResult; /** * Base class for a {@link Snippet} that is produced using a {@link Template} and @@ -45,18 +45,20 @@ protected TemplatedSnippet(String snippetName, Map attributes) { } @Override - public void document(String operation, MvcResult result) throws IOException { - WriterResolver writerResolver = (WriterResolver) result.getRequest() - .getAttribute(WriterResolver.class.getName()); - try (Writer writer = writerResolver.resolve(operation, this.snippetName)) { - Map model = document(result); + public void document(Operation operation) throws IOException { + WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get( + WriterResolver.class.getName()); + try (Writer writer = writerResolver + .resolve(operation.getName(), this.snippetName)) { + Map model = createModel(operation); model.putAll(this.attributes); - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); + TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() + .get(TemplateEngine.class.getName()); writer.append(templateEngine.compileTemplate(this.snippetName).render(model)); } } - protected abstract Map document(MvcResult result) throws IOException; + protected abstract Map createModel(Operation operation) + throws IOException; } \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index b8344b2f4..8af90689f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -91,28 +91,6 @@ public void customPort() { assertUriConfiguration("http", "localhost", 8081); } - @Test - public void customContextPathWithoutSlash() { - String contextPath = "context-path"; - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() - .withContextPath(contextPath).beforeMockMvcCreated(null, null); - postProcessor.postProcessRequest(this.request); - - assertUriConfiguration("http", "localhost", 8080); - assertThat(this.request.getContextPath(), equalTo("/" + contextPath)); - } - - @Test - public void customContextPathWithSlash() { - String contextPath = "/context-path"; - RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() - .withContextPath(contextPath).beforeMockMvcCreated(null, null); - postProcessor.postProcessRequest(this.request); - - assertUriConfiguration("http", "localhost", 8080); - assertThat(this.request.getContextPath(), equalTo(contextPath)); - } - @Test public void noContentLengthHeaderWhenRequestHasNotContent() { RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris() diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index b1cec398c..48a8a5665 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -19,14 +19,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; -import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; @@ -34,13 +29,14 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link CurlRequestSnippet} @@ -62,16 +58,16 @@ public class CurlRequestSnippetTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet() - .document("get-request", result(get("/foo"))); + new CurlRequestSnippet().document(new OperationBuilder("get-request").request( + "http://localhost/foo").build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document("non-get-request", - result(post("/foo"))); + new CurlRequestSnippet().document(new OperationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); } @Test @@ -79,8 +75,8 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document("request-with-content", - result(get("/foo").content("content"))); + new CurlRequestSnippet().document(new OperationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); } @Test @@ -89,37 +85,9 @@ public void requestWithQueryString() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document("request-with-query-string", - result(get("/foo?param=value"))); - } - - @Test - public void requestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("request-with-one-parameter").withContents( - codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); - new CurlRequestSnippet().document("request-with-one-parameter", - result(get("/foo").param("k1", "v1"))); - } - - @Test - public void requestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); - new CurlRequestSnippet().document( - "request-with-multiple-parameters", result(get("/foo").param("k1", "v1") - .param("k2", "v2").param("k1", "v1-bis"))); - } - - @Test - public void requestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest("request-with-url-encoded-parameter") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); - new CurlRequestSnippet().document( - "request-with-url-encoded-parameter", - result(get("/foo").param("k1", "foo bar&"))); + new CurlRequestSnippet().document(new OperationBuilder( + "request-with-query-string").request("http://localhost/foo?param=value") + .build()); } @Test @@ -127,8 +95,9 @@ public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); - new CurlRequestSnippet().document("post-request-with-one-parameter", - result(post("/foo").param("k1", "v1"))); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-one-parameter").request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test @@ -138,9 +107,9 @@ public void postRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document( - "post-request-with-multiple-parameters", - result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-multiple-parameters").request("http://localhost/foo") + .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test @@ -150,9 +119,10 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet().document( - "post-request-with-url-encoded-parameter", - result(post("/foo").param("k1", "a&b"))); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); } @Test @@ -160,8 +130,9 @@ public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document("put-request-with-one-parameter", - result(put("/foo").param("k1", "v1"))); + new CurlRequestSnippet().document(new OperationBuilder( + "put-request-with-one-parameter").request("http://localhost/foo") + .method("PUT").param("k1", "v1").build()); } @Test @@ -171,9 +142,10 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document( - "put-request-with-multiple-parameters", - result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); + new CurlRequestSnippet().document(new OperationBuilder( + "put-request-with-multiple-parameters").request("http://localhost/foo") + .method("PUT").param("k1", "v1").param("k1", "v1-bis").param("k2", "v2") + .build()); } @Test @@ -182,9 +154,9 @@ public void putRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet().document( - "put-request-with-url-encoded-parameter", - result(put("/foo").param("k1", "a&b"))); + new CurlRequestSnippet().document(new OperationBuilder( + "put-request-with-url-encoded-parameter").request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test @@ -193,92 +165,24 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document( - "request-with-headers", - result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", - "alpha"))); - } - - @Test - public void httpWithNonStandardPort() throws IOException { - this.snippet.expectCurlRequest("http-with-non-standard-port").withContents( - codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerPort(8080); - new CurlRequestSnippet().document("http-with-non-standard-port", - result(request)); + new CurlRequestSnippet().document(new OperationBuilder("request-with-headers") + .request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test - public void httpsWithStandardPort() throws IOException { - this.snippet.expectCurlRequest("https-with-standard-port").withContents( - codeBlock("bash").content("$ curl 'https://localhost/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerPort(443); - request.setScheme("https"); - new CurlRequestSnippet().document("https-with-standard-port", - result(request)); - } - - @Test - public void httpsWithNonStandardPort() throws IOException { - this.snippet.expectCurlRequest("https-with-non-standard-port").withContents( - codeBlock("bash").content("$ curl 'https://localhost:8443/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerPort(8443); - request.setScheme("https"); - new CurlRequestSnippet().document("https-with-non-standard-port", - result(request)); - } - - @Test - public void requestWithCustomHost() throws IOException { - this.snippet.expectCurlRequest("request-with-custom-host").withContents( - codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerName("api.example.com"); - new CurlRequestSnippet().document("request-with-custom-host", - result(request)); - } - - @Test - public void requestWithContextPathWithSlash() throws IOException { - this.snippet.expectCurlRequest("request-with-custom-context-with-slash") - .withContents( - codeBlock("bash").content( - "$ curl 'http://api.example.com/v3/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerName("api.example.com"); - request.setContextPath("/v3"); - new CurlRequestSnippet().document( - "request-with-custom-context-with-slash", result(request)); - } - - @Test - public void requestWithContextPathWithoutSlash() throws IOException { - this.snippet.expectCurlRequest("request-with-custom-context-without-slash") - .withContents( - codeBlock("bash").content( - "$ curl 'http://api.example.com/v3/foo' -i")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerName("api.example.com"); - request.setContextPath("v3"); - new CurlRequestSnippet().document( - "request-with-custom-context-without-slash", result(request)); - } - - @Test - public void multipartPostWithNoOriginalFilename() throws IOException { + public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("metadata", - "{\"description\": \"foo\"}".getBytes()); - new CurlRequestSnippet().document( - "multipart-post-no-original-filename", - result(fileUpload("/upload").file(multipartFile))); + new CurlRequestSnippet().document(new OperationBuilder( + "multipart-post-no-original-filename").request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @Test @@ -288,12 +192,13 @@ public void multipartPostWithContentType() throws IOException { + "'image=@documents/images/example.png;type=image/png'"; this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( codeBlock("bash").content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, - "bytes".getBytes()); - new CurlRequestSnippet().document( - "multipart-post-with-content-type", - result(fileUpload("/upload").file(multipartFile))); + new CurlRequestSnippet().document(new OperationBuilder( + "multipart-post-with-content-type").request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -303,10 +208,11 @@ public void multipartPost() throws IOException { + "'image=@documents/images/example.png'"; this.snippet.expectCurlRequest("multipart-post").withContents( codeBlock("bash").content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", null, "bytes".getBytes()); - new CurlRequestSnippet().document("multipart-post", - result(fileUpload("/upload").file(multipartFile))); + new CurlRequestSnippet().document(new OperationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -315,14 +221,15 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest("multipart-post").withContents( + this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( codeBlock("bash").content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", null, "bytes".getBytes()); - new CurlRequestSnippet().document( - "multipart-post", - result(fileUpload("/upload").file(multipartFile) - .param("a", "apple", "avocado").param("b", "banana"))); + new CurlRequestSnippet().document(new OperationBuilder( + "multipart-post-with-parameters").request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test @@ -337,8 +244,11 @@ public void customAttributes() throws IOException { "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - new CurlRequestSnippet(attributes(key("title").value( - "curl request title"))).document("custom-attributes", result(request)); + new CurlRequestSnippet(attributes(key("title").value("curl request title"))) + .document(new OperationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 4d5693393..3eacda6e0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -19,14 +19,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; -import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; import static org.springframework.web.bind.annotation.RequestMethod.PUT; @@ -38,12 +33,11 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link HttpRequestSnippet} @@ -65,8 +59,8 @@ public void getRequest() throws IOException { httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( "Alpha", "a")); - new HttpRequestSnippet().document("get-request", result(get("/foo") - .header("Alpha", "a"))); + new HttpRequestSnippet().document(new OperationBuilder("get-request") + .request("http://localhost/foo").header("Alpha", "a").build()); } @Test @@ -74,17 +68,9 @@ public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string").withContents( httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document("get-request-with-query-string", - result(get("/foo?bar=baz"))); - } - - @Test - public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("get-request-with-parameter").withContents( - httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost")); - - new HttpRequestSnippet().document("get-request-with-parameter", - result(get("/foo").param("b&r", "baz"))); + new HttpRequestSnippet().document(new OperationBuilder( + "get-request-with-query-string").request("http://localhost/foo?bar=baz") + .build()); } @Test @@ -93,8 +79,9 @@ public void postRequestWithContent() throws IOException { httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - new HttpRequestSnippet().document("post-request-with-content", - result(post("/foo").content("Hello, world"))); + new HttpRequestSnippet().document(new OperationBuilder( + "post-request-with-content").request("http://localhost/foo") + .method("POST").content("Hello, world").build()); } @Test @@ -104,8 +91,9 @@ public void postRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document("post-request-with-parameter", - result(post("/foo").param("b&r", "baz").param("a", "alpha"))); + new HttpRequestSnippet().document(new OperationBuilder( + "post-request-with-parameter").request("http://localhost/foo") + .method("POST").param("b&r", "baz").param("a", "alpha").build()); } @Test @@ -114,8 +102,10 @@ public void putRequestWithContent() throws IOException { httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - new HttpRequestSnippet().document("put-request-with-content", - result(put("/foo").content("Hello, world"))); + new HttpRequestSnippet() + .document(new OperationBuilder("put-request-with-content") + .request("http://localhost/foo").method("PUT") + .content("Hello, world").build()); } @Test @@ -125,8 +115,9 @@ public void putRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document("put-request-with-parameter", - result(put("/foo").param("b&r", "baz").param("a", "alpha"))); + new HttpRequestSnippet().document(new OperationBuilder( + "put-request-with-parameter").request("http://localhost/foo") + .method("PUT").param("b&r", "baz").param("a", "alpha").build()); } @Test @@ -139,10 +130,10 @@ public void multipartPost() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", null, "<< data >>".getBytes()); - new HttpRequestSnippet().document("multipart-post", - result(fileUpload("/upload").file(multipartFile))); + new HttpRequestSnippet().document(new OperationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -156,18 +147,18 @@ public void multipartPostWithParameters() throws IOException { String filePart = createPart(String.format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest("multipart-post").withContents( + this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( httpRequest(POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", null, "<< data >>".getBytes()); - new HttpRequestSnippet().document( - "multipart-post", - result(fileUpload("/upload").file(multipartFile) - .param("a", "apple", "avocado").param("b", "banana"))); + new HttpRequestSnippet().document(new OperationBuilder( + "multipart-post-with-parameters").request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -181,50 +172,37 @@ public void multipartPostWithContentType() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); - MockMultipartFile multipartFile = new MockMultipartFile("image", - "documents/images/example.png", MediaType.IMAGE_PNG_VALUE, - "<< data >>".getBytes()); - new HttpRequestSnippet().document( - "multipart-post-with-content-type", - result(fileUpload("/upload").file(multipartFile))); - } - - @Test - public void getRequestWithCustomServerName() throws IOException { - this.snippet.expectHttpRequest("get-request-custom-server-name").withContents( - httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - request.setServerName("api.example.com"); - - new HttpRequestSnippet().document("get-request-custom-server-name", - result(request)); + new HttpRequestSnippet().document(new OperationBuilder( + "multipart-post-with-content-type").request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); } @Test public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - - new HttpRequestSnippet().document("get-request-custom-host", - result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); + new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host") + .request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com").build()); } @Test public void requestWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( containsString("Title for the request")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("http-request")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - new HttpRequestSnippet(attributes(key("title").value( - "Title for the request"))).document("request-with-snippet-attributes", - result(request)); + new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) + .document(new OperationBuilder("request-with-snippet-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } private String createPart(String content) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index f9f7bac5e..068d6b888 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -24,20 +24,19 @@ import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; -import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link HttpResponseSnippet} @@ -53,18 +52,16 @@ public class HttpResponseSnippetTests { @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); - new HttpResponseSnippet().document("basic-response", result()); + new HttpResponseSnippet() + .document(new OperationBuilder("basic-response").build()); } @Test public void nonOkResponse() throws IOException { this.snippet.expectHttpResponse("non-ok-response").withContents( httpResponse(BAD_REQUEST)); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(BAD_REQUEST.value()); - new HttpResponseSnippet().document("non-ok-response", - result(response)); + new HttpResponseSnippet().document(new OperationBuilder("non-ok-response") + .response().status(BAD_REQUEST.value()).build()); } @Test @@ -73,39 +70,33 @@ public void responseWithHeaders() throws IOException { httpResponse(OK) // .header("Content-Type", "application/json") // .header("a", "alpha")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - new HttpResponseSnippet().document("response-with-headers", - result(response)); + new HttpResponseSnippet().document(new OperationBuilder("response-with-headers") + .response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test public void responseWithContent() throws IOException { this.snippet.expectHttpResponse("response-with-content").withContents( httpResponse(OK).content("content")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("content"); - new HttpResponseSnippet().document("response-with-content", - result(response)); + new HttpResponseSnippet().document(new OperationBuilder("response-with-content") + .response().content("content").build()); } @Test public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( containsString("Title for the response")); - MockHttpServletRequest request = new MockHttpServletRequest(); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("http-response")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - new HttpResponseSnippet(attributes(key("title").value( - "Title for the response"))).document("response-with-snippet-attributes", - result(request)); + new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) + .document(new OperationBuilder("response-with-snippet-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 1c258a262..161fefbf1 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -26,8 +26,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.StandardOperationResponse; /** * Tests for {@link ContentTypeLinkExtractor}. @@ -39,12 +41,11 @@ public class ContentTypeLinkExtractorTests { @Rule public ExpectedException thrown = ExpectedException.none(); - private final MockHttpServletResponse response = new MockHttpServletResponse(); - @Test public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks(this.response); + new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse( + HttpStatus.OK, new HttpHeaders(), null)); } @Test @@ -52,9 +53,12 @@ public void extractorCalledWithMatchingContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); - this.response.setContentType("application/json"); - new ContentTypeLinkExtractor(extractors).extractLinks(this.response); - verify(extractor).extractLinks(this.response); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + httpHeaders, null); + new ContentTypeLinkExtractor(extractors).extractLinks(response); + verify(extractor).extractLinks(response); } @Test @@ -62,9 +66,12 @@ public void extractorCalledWithCompatibleContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); - this.response.setContentType("application/json;foo=bar"); - new ContentTypeLinkExtractor(extractors).extractLinks(this.response); - verify(extractor).extractLinks(this.response); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar")); + StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + httpHeaders, null); + new ContentTypeLinkExtractor(extractors).extractLinks(response); + verify(extractor).extractLinks(response); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 9e6256d7f..6711ae19b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -31,7 +30,9 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperationResponse; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -105,11 +106,9 @@ private void assertLinks(List expectedLinks, Map> actua assertEquals(expectedLinksByRel, actualLinks); } - private MockHttpServletResponse createResponse(String contentName) throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)), - response.getWriter()); - return response; + private OperationResponse createResponse(String contentName) throws IOException { + return new StandardOperationResponse(HttpStatus.OK, null, + FileCopyUtils.copyToByteArray(getPayloadFile(contentName))); } private File getPayloadFile(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 99ad2bc60..97979bdc9 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -23,7 +23,6 @@ import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; import java.util.Arrays; @@ -34,12 +33,13 @@ import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -61,9 +61,9 @@ public void undocumentedLink() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", - "bar")), Collections. emptyList()).document( - "undocumented-link", result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections. emptyList()).document(new OperationBuilder( + "undocumented-link").build()); } @Test @@ -71,9 +71,9 @@ public void missingLink() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), - Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( - "missing-link", result()); + new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") + .description("bar"))).document(new OperationBuilder("missing-link") + .build()); } @Test @@ -81,9 +81,9 @@ public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", - "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar") - .optional())).document("documented-optional-link", result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), + Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) + .document(new OperationBuilder("documented-optional-link").build()); } @Test @@ -91,9 +91,9 @@ public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( // tableWithHeader("Relation", "Description") // .row("foo", "bar")); - new LinksSnippet(new StubLinkExtractor(), - Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document("missing-optional-link", result()); + new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") + .description("bar").optional())).document(new OperationBuilder( + "missing-optional-link").build()); } @Test @@ -102,9 +102,10 @@ public void undocumentedLinkAndMissingLink() throws IOException { this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", - "alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document("undocumented-link-and-missing-link", result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(new OperationBuilder("undocumented-link-and-missing-link") + .build()); } @Test @@ -113,11 +114,11 @@ public void documentedLinks() throws IOException { tableWithHeader("Relation", "Description") // .row("a", "one") // .row("b", "two")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", - "alpha"), new Link("b", "bravo")), Arrays.asList( + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))).document("documented-links", - result()); + new LinkDescriptor("b").description("two"))) + .document(new OperationBuilder("documented-links").build()); } @Test @@ -134,33 +135,34 @@ public void linksWithCustomDescriptorAttributes() throws IOException { "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", - "alpha"), new Link("b", "bravo")), Arrays.asList( + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one").attributes( key("foo").value("alpha")), new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo")))).document( - "links-with-custom-descriptor-attributes", result(request)); + key("foo").value("bravo")))).document(new OperationBuilder( + "links-with-custom-descriptor-attributes").attribute( + TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .build()); } @Test public void linksWithCustomAttributes() throws IOException { this.snippet.expectLinks("links-with-custom-attributes").withContents( startsWith(".Title for the links")); - MockHttpServletRequest request = new MockHttpServletRequest(); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("links")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/links-with-title.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", - "alpha"), new Link("b", "bravo")), attributes(key("title").value( + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), attributes(key("title").value( "Title for the links")), Arrays.asList( new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))).document( - "links-with-custom-attributes", result(request)); + new LinkDescriptor("b").description("two"))) + .document(new OperationBuilder("links-with-custom-attributes").attribute( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } private static class StubLinkExtractor implements LinkExtractor { @@ -168,7 +170,7 @@ private static class StubLinkExtractor implements LinkExtractor { private MultiValueMap linksByRel = new LinkedMultiValueMap(); @Override - public MultiValueMap extractLinks(MockHttpServletResponse response) + public MultiValueMap extractLinks(OperationResponse response) throws IOException { return this.linksByRel; } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java new file mode 100644 index 000000000..9d2866100 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java @@ -0,0 +1,220 @@ +package org.springframework.restdocs.hypermedia; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.util.Arrays; + +import javax.servlet.http.Part; + +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockServletContext; +import org.springframework.restdocs.operation.MockMvcOperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +/** + * Tests for {@link MockMvcOperationRequestFactory} + * + * @author Andy Wilkinson + */ +public class MockMvcOperationRequestFactoryTests { + + private final MockMvcOperationRequestFactory factory = new MockMvcOperationRequestFactory(); + + @Test + public void httpRequest() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo")); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void httpRequestWithCustomPort() throws Exception { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") + .buildRequest(new MockServletContext()); + mockRequest.setServerPort(8080); + OperationRequest request = this.factory.createOperationRequest(mockRequest); + assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void requestWithContextPath() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get( + "/foo/bar").contextPath("/foo")); + assertThat(request.getUri(), is(URI.create("http://localhost/foo/bar"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void requestWithHeaders() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo").header("a", "alpha", "apple").header("b", "bravo")); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getHeaders(), hasEntry("a", Arrays.asList("alpha", "apple"))); + assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo"))); + } + + @Test + public void httpsRequest() throws Exception { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") + .buildRequest(new MockServletContext()); + mockRequest.setScheme("https"); + mockRequest.setServerPort(443); + OperationRequest request = this.factory.createOperationRequest(mockRequest); + assertThat(request.getUri(), is(URI.create("https://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void httpsRequestWithCustomPort() throws Exception { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") + .buildRequest(new MockServletContext()); + mockRequest.setScheme("https"); + mockRequest.setServerPort(8443); + OperationRequest request = this.factory.createOperationRequest(mockRequest); + assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void getRequestWithParametersProducesUriWithQueryString() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo").param("a", "alpha", "apple").param("b", "br&vo")); + assertThat(request.getUri(), + is(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo"))); + assertThat(request.getParameters().size(), is(2)); + assertThat(request.getParameters(), + hasEntry("a", Arrays.asList("alpha", "apple"))); + assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void getRequestWithQueryStringPopulatesParameters() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo?a=alpha&b=bravo")); + assertThat(request.getUri(), + is(URI.create("http://localhost/foo?a=alpha&b=bravo"))); + assertThat(request.getParameters().size(), is(2)); + assertThat(request.getParameters(), hasEntry("a", Arrays.asList("alpha"))); + assertThat(request.getParameters(), hasEntry("b", Arrays.asList("bravo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + } + + @Test + public void postRequestWithParameters() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .post("/foo").param("a", "alpha", "apple").param("b", "br&vo")); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.POST)); + assertThat(request.getParameters().size(), is(2)); + assertThat(request.getParameters(), + hasEntry("a", Arrays.asList("alpha", "apple"))); + assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo"))); + } + + @Test + public void mockMultipartFileUpload() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .fileUpload("/foo").file( + new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.POST)); + assertThat(request.getParts().size(), is(1)); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName(), is(equalTo("file"))); + assertThat(part.getSubmittedFileName(), is(nullValue())); + assertThat(part.getHeaders().isEmpty(), is(true)); + assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + } + + @Test + public void mockMultipartFileUploadWithContentType() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .fileUpload("/foo").file( + new MockMultipartFile("file", "original", "image/png", + new byte[] { 1, 2, 3, 4 }))); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.POST)); + assertThat(request.getParts().size(), is(1)); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName(), is(equalTo("file"))); + assertThat(part.getSubmittedFileName(), is(equalTo("original"))); + assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG)); + assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + } + + @Test + public void requestWithPart() throws Exception { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") + .buildRequest(new MockServletContext()); + Part mockPart = mock(Part.class); + when(mockPart.getHeaderNames()).thenReturn(Arrays.asList("a", "b")); + when(mockPart.getHeaders("a")).thenReturn(Arrays.asList("alpha")); + when(mockPart.getHeaders("b")).thenReturn(Arrays.asList("bravo", "banana")); + when(mockPart.getInputStream()).thenReturn( + new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + when(mockPart.getName()).thenReturn("part-name"); + when(mockPart.getSubmittedFileName()).thenReturn("submitted.txt"); + mockRequest.addPart(mockPart); + OperationRequest request = this.factory.createOperationRequest(mockRequest); + assertThat(request.getParts().size(), is(1)); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName(), is(equalTo("part-name"))); + assertThat(part.getSubmittedFileName(), is(equalTo("submitted.txt"))); + assertThat(part.getHeaders().getContentType(), is(nullValue())); + assertThat(part.getHeaders().get("a"), contains("alpha")); + assertThat(part.getHeaders().get("b"), contains("bravo", "banana")); + assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + } + + @Test + public void requestWithPartWithContentType() throws Exception { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") + .buildRequest(new MockServletContext()); + Part mockPart = mock(Part.class); + when(mockPart.getHeaderNames()).thenReturn(Arrays.asList("a", "b")); + when(mockPart.getHeaders("a")).thenReturn(Arrays.asList("alpha")); + when(mockPart.getHeaders("b")).thenReturn(Arrays.asList("bravo", "banana")); + when(mockPart.getInputStream()).thenReturn( + new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + when(mockPart.getName()).thenReturn("part-name"); + when(mockPart.getSubmittedFileName()).thenReturn("submitted.png"); + when(mockPart.getContentType()).thenReturn("image/png"); + mockRequest.addPart(mockPart); + OperationRequest request = this.factory.createOperationRequest(mockRequest); + assertThat(request.getParts().size(), is(1)); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName(), is(equalTo("part-name"))); + assertThat(part.getSubmittedFileName(), is(equalTo("submitted.png"))); + assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG)); + assertThat(part.getHeaders().get("a"), contains("alpha")); + assertThat(part.getHeaders().get("b"), contains("bravo", "banana")); + assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + } + + private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) + throws Exception { + return this.factory.createOperationRequest(builder + .buildRequest(new MockServletContext())); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index fb257e54e..bec7ee7eb 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -21,12 +21,10 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; import java.util.Arrays; @@ -36,14 +34,14 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link RequestFieldsSnippet} @@ -68,9 +66,9 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))).document( - "map-request-with-fields", - result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"))); + fieldWithPath("a").description("three"))).document(new OperationBuilder( + "map-request-with-fields").request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test @@ -83,10 +81,9 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a") - .description("three"))).document( - "array-request-with-fields", - result(get("/foo").content( - "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"))); + .description("three"))).document(new OperationBuilder( + "array-request-with-fields").request("http://localhost") + .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); } @Test @@ -95,8 +92,9 @@ public void undocumentedRequestField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections. emptyList()).document( - "undocumented-request-field", result(get("/foo").content("{\"a\": 5}"))); + new RequestFieldsSnippet(Collections. emptyList()) + .document(new OperationBuilder("undocumented-request-field") + .request("http://localhost").content("{\"a\": 5}").build()); } @Test @@ -106,15 +104,17 @@ public void missingRequestField() throws IOException { .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document("missing-request-fields", result(get("/foo").content("{}"))); + .document(new OperationBuilder("missing-request-fields") + .request("http://localhost").content("{}").build()); } @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .optional())).document("missing-optional-request-field-with-no-type", - result(get("/foo").content("{ }"))); + .optional())).document(new OperationBuilder( + "missing-optional-request-field-with-no-type") + .request("http://localhost").content("{ }").build()); } @Test @@ -127,8 +127,10 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document("undocumented-request-field-and-missing-request-field", - result(get("/foo").content("{ \"a\": { \"c\": 5 }}"))); + .document(new OperationBuilder( + "undocumented-request-field-and-missing-request-field") + .request("http://localhost").content("{ \"a\": { \"c\": 5 }}") + .build()); } @Test @@ -142,18 +144,17 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("request-fields")).thenReturn( snippetResource("request-fields-with-extra-column")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()); new RequestFieldsSnippet(Arrays.asList( fieldWithPath("a.b").description("one").attributes( key("foo").value("alpha")), fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document( - "request-fields-with-custom-descriptor-attributes", result(request)); + key("foo").value("charlie")))).document(new OperationBuilder( + "request-fields-with-custom-descriptor-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test @@ -163,14 +164,12 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("request-fields")).thenReturn( snippetResource("request-fields-with-title")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.setContent("{\"a\": \"foo\"}".getBytes()); - MockHttpServletResponse response = new MockHttpServletResponse(); new RequestFieldsSnippet(attributes(key("title").value("Custom title")), - Arrays.asList(fieldWithPath("a").description("one"))).document( - "request-fields-with-custom-attributes", result(request, response)); + Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("request-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").content("{\"a\": \"foo\"}").build()); } @Test @@ -183,10 +182,12 @@ public void xmlRequestFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))).document( - "xml-request", - result(get("/foo").content("5charlie").contentType( - MediaType.APPLICATION_XML))); + fieldWithPath("a").description("three").type("a"))) + .document(new OperationBuilder("xml-request") + .request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -195,19 +196,23 @@ public void undocumentedXmlRequestField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections. emptyList()).document( - "undocumented-xml-request-field", - result(get("/foo").content("5").contentType( - MediaType.APPLICATION_XML))); + new RequestFieldsSnippet(Collections. emptyList()) + .document(new OperationBuilder("undocumented-xml-request-field") + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document("missing-xml-request", - result(get("/foo").contentType(MediaType.APPLICATION_XML) - .content("5"))); + .document(new OperationBuilder("missing-xml-request") + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -217,10 +222,11 @@ public void missingXmlRequestField() throws IOException { .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document( - "missing-xml-request-fields", - result(get("/foo").contentType(MediaType.APPLICATION_XML).content( - ""))); + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-request-fields").request("http://localhost") + .content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -233,9 +239,12 @@ public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOExce .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document("undocumented-xml-request-field-and-missing-xml-request-field", - result(get("/foo").contentType(MediaType.APPLICATION_XML) - .content("5"))); + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field") + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index a768f05b0..9018f96d7 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -24,7 +24,6 @@ import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; import java.io.IOException; import java.util.Arrays; @@ -34,14 +33,15 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link PayloadDocumentation} @@ -66,18 +66,18 @@ public void mapResponseWithFields() throws IOException { .row("assets[]", "Object", "four") // .row("assets[].id", "Number", "five") // .row("assets[].name", "String", "six")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets") .description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), - fieldWithPath("assets[].name").description("six"))).document( - "map-response-with-fields", result(response)); + fieldWithPath("assets[].name").description("six"))) + .document(new OperationBuilder("map-response-with-fields") + .response() + .content( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); } @Test @@ -87,14 +87,12 @@ public void arrayResponseWithFields() throws IOException { .row("[]a.b", "Number", "one") // .row("[]a.c", "String", "two") // .row("[]a", "Object", "three")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter() - .append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]"); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") .description("two"), fieldWithPath("[]a").description("three"))) - .document("array-response-with-fields", result(response)); + .document(new OperationBuilder("array-response-with-fields").response() + .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") + .build()); } @Test @@ -102,11 +100,9 @@ public void arrayResponse() throws IOException { this.snippet.expectResponseFields("array-response").withContents( // tableWithHeader("Path", "Type", "Description") // .row("[]", "String", "one")); - - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("[\"a\", \"b\", \"c\"]"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document("array-response", result(response)); + .document(new OperationBuilder("array-response").response() + .content("[\"a\", \"b\", \"c\"]").build()); } @Test @@ -120,19 +116,17 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("response-fields")).thenReturn( snippetResource("response-fields-with-extra-column")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}"); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("a.b").description("one").attributes( key("foo").value("alpha")), fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document( - "response-fields-with-custom-attributes", result(request, response)); + key("foo").value("charlie")))).document(new OperationBuilder( + "response-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test @@ -142,14 +136,12 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("response-fields")).thenReturn( snippetResource("response-fields-with-title")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getOutputStream().print("{\"a\": \"foo\"}"); new ResponseFieldsSnippet(attributes(key("title").value("Custom title")), - Arrays.asList(fieldWithPath("a").description("one"))).document( - "response-fields-with-custom-attributes", result(request, response)); + Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("response-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .content("{\"a\": \"foo\"}").build()); } @Test @@ -159,13 +151,14 @@ public void xmlResponseFields() throws IOException { .row("a/b", "b", "one") // .row("a/c", "c", "two") // .row("a", "a", "three")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_XML_VALUE); - response.getOutputStream().print("5charlie"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))).document( - "xml-response", result(response)); + fieldWithPath("a").description("three").type("a"))) + .document(new OperationBuilder("xml-response") + .response() + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -177,8 +170,12 @@ public void undocumentedXmlResponseField() throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); response.setContentType(MediaType.APPLICATION_XML_VALUE); response.getOutputStream().print("5"); - new ResponseFieldsSnippet(Collections. emptyList()).document( - "undocumented-xml-response-field", result(response)); + new ResponseFieldsSnippet(Collections. emptyList()) + .document(new OperationBuilder("undocumented-xml-response-field") + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -188,7 +185,11 @@ public void xmlResponseFieldWithNoType() throws IOException { response.setContentType(MediaType.APPLICATION_XML_VALUE); response.getOutputStream().print("5"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document("xml-response-no-field-type", result(response)); + .document(new OperationBuilder("xml-response-no-field-type") + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -201,8 +202,10 @@ public void missingXmlResponseField() throws IOException { response.setContentType(MediaType.APPLICATION_XML_VALUE); response.getOutputStream().print(""); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document( - "missing-xml-response-field", result(response)); + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-response-field").response().content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -219,8 +222,12 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() response.setContentType(MediaType.APPLICATION_XML_VALUE); response.getOutputStream().print("5"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document("undocumented-xml-request-field-and-missing-xml-request-field", - result(response)); + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field") + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 43ed107b6..c4204d8c4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -25,8 +25,6 @@ import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.restdocs.test.TestRequestBuilders.get; import java.io.IOException; import java.util.Arrays; @@ -36,12 +34,12 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link PathParametersSnippet} @@ -63,7 +61,8 @@ public void undocumentedPathParameter() throws IOException { this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not documented: [a]")); new PathParametersSnippet(Collections. emptyList()) - .document("undocumented-path-parameter", result(get("/{a}/", "alpha"))); + .document(new OperationBuilder("undocumented-path-parameter").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/").build()); } @Test @@ -72,8 +71,9 @@ public void missingPathParameter() throws IOException { this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not found in the request: [a]")); new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - "missing-path-parameter", result(get("/"))); + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("missing-path-parameter").attribute( + "org.springframework.restdocs.urlTemplate", "/").build()); } @Test @@ -83,8 +83,10 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " not documented: [b]. Path parameters with the following" + " names were not found in the request: [a]")); new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - "undocumented-and-missing-path-parameters", result(get("/{b}", "bravo"))); + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("undocumented-and-missing-path-parameters") + .attribute("org.springframework.restdocs.urlTemplate", "/{b}") + .build()); } @Test @@ -94,11 +96,9 @@ public void pathParameters() throws IOException { "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document( - "path-parameters", - result(get("/{a}/{b}", "alpha", "banana").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext(null)))); + .description("two"))).document(new OperationBuilder( + "path-parameters").attribute("org.springframework.restdocs.urlTemplate", + "/{a}/{b}").build()); } @Test @@ -109,11 +109,9 @@ public void pathParametersWithQueryString() throws IOException { .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document( - "path-parameters-with-query-string", - result(get("/{a}/{b}?foo=bar", "alpha", "banana").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext(null)))); + .description("two"))).document(new OperationBuilder( + "path-parameters-with-query-string").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); } @Test @@ -127,11 +125,12 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { snippetResource("path-parameters-with-extra-column")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))).document( - "path-parameters-with-custom-descriptor-attributes", - result(get("{a}/{b}", "alpha", "bravo").requestAttr( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)))); + .description("two").attributes(key("foo").value("bravo")))) + .document(new OperationBuilder( + "path-parameters-with-custom-descriptor-attributes") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } @Test @@ -147,11 +146,10 @@ public void pathParametersWithCustomAttributes() throws IOException { parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document( - "path-parameters-with-custom-attributes", - result(get("{a}/{b}", "alpha", "bravo").requestAttr( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)))); + .document(new OperationBuilder("path-parameters-with-custom-attributes") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index fd74544dd..0ec85773d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -24,8 +24,6 @@ import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.StubMvcResult.result; -import static org.springframework.restdocs.test.TestRequestBuilders.get; import java.io.IOException; import java.util.Arrays; @@ -35,13 +33,12 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.config.RestDocumentationContext; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; /** * Tests for {@link RequestParametersSnippet} @@ -63,7 +60,8 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections. emptyList()) - .document("undocumented-parameter", result(get("/").param("a", "alpha"))); + .document(new OperationBuilder("undocumented-parameter") + .request("http://localhost").param("a", "alpha").build()); } @Test @@ -73,7 +71,8 @@ public void missingParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document("missing-parameter", result(get("/"))); + "one"))).document(new OperationBuilder("missing-parameter").request( + "http://localhost").build()); } @Test @@ -84,36 +83,21 @@ public void undocumentedAndMissingParameters() throws IOException { + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document("undocumented-and-missing-parameters", result(get("/") - .param("b", "bravo"))); + "one"))).document(new OperationBuilder( + "undocumented-and-missing-parameters").request("http://localhost") + .param("b", "bravo").build()); } @Test - public void requestParameterSnippetFromRequestParameters() throws IOException { - this.snippet.expectRequestParameters( - "request-parameter-snippet-request-parameters").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document( - "request-parameter-snippet-request-parameters", - result(get("/").param("a", "bravo").param("b", "bravo"))); - } - - @Test - public void requestParameterSnippetFromRequestUriQueryString() throws IOException { - this.snippet.expectRequestParameters( - "request-parameter-snippet-request-uri-query-string").withContents( + public void requestParameters() throws IOException { + this.snippet.expectRequestParameters("request-parameters").withContents( tableWithHeader("Parameter", "Description").row("a", "one").row("b", "two")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document( - "request-parameter-snippet-request-uri-query-string", - result(get("/?a=alpha&b=bravo").requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext(null)))); + .description("two"))).document(new OperationBuilder( + "request-parameters").request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); } @Test @@ -125,17 +109,15 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("request-parameters")).thenReturn( snippetResource("request-parameters-with-extra-column")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.addParameter("a", "bravo"); - request.addParameter("b", "bravo"); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document( - "request-parameters-with-custom-descriptor-attributes", result(request)); + key("foo").value("bravo")))).document(new OperationBuilder( + "request-parameters-with-custom-descriptor-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .param("a", "alpha").param("b", "bravo").build()); } @Test @@ -145,18 +127,18 @@ public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("request-parameters")).thenReturn( snippetResource("request-parameters-with-title")); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); - request.addParameter("a", "bravo"); - request.addParameter("b", "bravo"); new RequestParametersSnippet( attributes(key("title").value("The title")), Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document("request-parameters-with-custom-attributes", result(request)); + .document(new OperationBuilder( + "request-parameters-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").param("a", "alpha") + .param("b", "bravo").build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java index dd07830a8..09487a3d9 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java @@ -38,6 +38,7 @@ public class ContentModifyingResponsePostProcessorTests { public void contentCanBeModified() throws Exception { MockHttpServletResponse modified = this.postProcessor.postProcess(this.original); assertThat(modified.getContentAsString(), is(equalTo("modified"))); + assertThat(modified.getContentAsByteArray(), is(equalTo("modified".getBytes()))); } @Test @@ -47,14 +48,6 @@ public void nonContentMethodsAreDelegated() throws Exception { assertThat(modified.getHeader("a"), is(equalTo("alpha"))); } - @Test - public void getContentAsByteArrayIsUnsupported() throws Exception { - this.thrown.expect(UnsupportedOperationException.class); - this.thrown.expectMessage(equalTo("Following modification, the response's" - + " content should be accessed as a String")); - this.postProcessor.postProcess(this.original).getContentAsByteArray(); - } - private static final class TestContentModifyingResponsePostProcessor extends ContentModifyingReponsePostProcessor { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java new file mode 100644 index 000000000..fbb193377 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -0,0 +1,211 @@ +package org.springframework.restdocs.test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.config.RestDocumentationContext; +import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationRequestPart; +import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +public class OperationBuilder { + + private final Map attributes = new HashMap<>(); + + private final OperationResponseBuilder responseBuilder = new OperationResponseBuilder(); + + private final String name; + + private OperationRequestBuilder requestBuilder; + + public OperationBuilder(String name) { + this.name = name; + } + + public OperationRequestBuilder request(String uri) { + this.requestBuilder = new OperationRequestBuilder(uri); + return this.requestBuilder; + } + + public OperationResponseBuilder response() { + return this.responseBuilder; + } + + public OperationBuilder attribute(String name, Object value) { + this.attributes.put(name, value); + return this; + } + + public Operation build() { + if (this.attributes.get(TemplateEngine.class.getName()) == null) { + this.attributes.put(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + } + RestDocumentationContext context = new RestDocumentationContext(null); + this.attributes.put(RestDocumentationContext.class.getName(), context); + this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolver(context))); + return new StandardOperation(this.name, + (this.requestBuilder == null ? new OperationRequestBuilder( + "http://localhost/").buildRequest() : this.requestBuilder + .buildRequest()), + this.responseBuilder.buildResponse(), this.attributes); + } + + public class OperationRequestBuilder { + + private URI requestUri = URI.create("http://localhost/"); + + private HttpMethod method = HttpMethod.GET; + + private byte[] content = new byte[0]; + + private HttpHeaders headers = new HttpHeaders(); + + private Parameters parameters = new Parameters(); + + private List partBuilders = new ArrayList<>(); + + public OperationRequestBuilder(String uri) { + this.requestUri = URI.create(uri); + } + + private OperationRequest buildRequest() { + List parts = new ArrayList<>(); + for (OperationRequestPartBuilder builder : this.partBuilders) { + parts.add(builder.buildPart()); + } + return new StandardOperationRequest(this.requestUri, this.method, + this.content, this.headers, this.parameters, parts); + } + + public Operation build() { + return OperationBuilder.this.build(); + } + + public OperationRequestBuilder method(String method) { + this.method = HttpMethod.valueOf(method); + return this; + } + + public OperationRequestBuilder content(String content) { + this.content = content.getBytes(); + return this; + } + + public OperationRequestBuilder param(String name, String... values) { + for (String value : values) { + this.parameters.add(name, value); + } + return this; + } + + public OperationRequestBuilder header(String name, String value) { + this.headers.add(name, value); + return this; + } + + public OperationRequestPartBuilder part(String name, byte[] content) { + OperationRequestPartBuilder partBuilder = new OperationRequestPartBuilder( + name, content); + this.partBuilders.add(partBuilder); + return partBuilder; + } + + public class OperationRequestPartBuilder { + + private final String name; + + private final byte[] content; + + private String submittedFileName; + + private HttpHeaders headers = new HttpHeaders(); + + private OperationRequestPartBuilder(String name, byte[] content) { + this.name = name; + this.content = content; + } + + public OperationRequestPartBuilder submittedFileName(String submittedFileName) { + this.submittedFileName = submittedFileName; + return this; + } + + public OperationRequestBuilder and() { + return OperationRequestBuilder.this; + } + + public Operation build() { + return OperationBuilder.this.build(); + } + + private OperationRequestPart buildPart() { + return new StandardOperationRequestPart(this.name, + this.submittedFileName, this.content, this.headers); + } + + public OperationRequestPartBuilder header(String name, String value) { + this.headers.add(name, value); + return this; + } + } + } + + public class OperationResponseBuilder { + + private HttpStatus status = HttpStatus.OK; + + private HttpHeaders headers = new HttpHeaders(); + + private byte[] content = new byte[0]; + + private OperationResponse buildResponse() { + return new StandardOperationResponse(this.status, this.headers, this.content); + } + + public OperationResponseBuilder status(int status) { + this.status = HttpStatus.valueOf(status); + return this; + } + + public OperationResponseBuilder header(String name, String value) { + this.headers.add(name, value); + return this; + } + + public OperationResponseBuilder content(byte[] content) { + this.content = content; + return this; + } + + public OperationResponseBuilder content(String content) { + this.content = content.getBytes(); + return this; + } + + public Operation build() { + return OperationBuilder.this.build(); + } + + } + +} From 9d8bbf05581536de16d6427b1d59eb197d892a11 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 2 Sep 2015 12:40:59 +0100 Subject: [PATCH 0119/1059] Add support for modifying a request prior to it being documented Prior to this commit it was not possible to modify a request prior to it being documented, only a response. This commit builds on the new Operation abstraction to simplify the existing response modification support and to add support for request modification. Closes gh-84 --- .../customizing-requests-and-responses.adoc | 56 ++++++++ .../docs/asciidoc/customizing-responses.adoc | 45 ------ docs/src/docs/asciidoc/index.adoc | 2 +- ...PostProcessing.java => Preprocessing.java} | 13 +- .../restdocs/ResponseModifier.java | 123 ---------------- .../restdocs/RestDocumentation.java | 68 +++++++-- .../RestDocumentationResultHandler.java | 69 ++++++++- .../operation/StandardOperationRequest.java | 2 +- .../operation/StandardOperationResponse.java | 2 +- .../preprocess/ContentModifier.java} | 25 ++-- ...ContentModifyingOperationPreprocessor.java | 65 +++++++++ ...elegatingOperationRequestPreprocessor.java | 57 ++++++++ ...legatingOperationResponsePreprocessor.java | 56 ++++++++ .../HeaderRemovingOperationPreprocessor.java | 63 ++++++++ .../LinkMaskingContentModifier.java} | 23 +-- .../preprocess/OperationPreprocessor.java | 47 ++++++ .../OperationRequestPreprocessor.java | 38 +++++ .../OperationResponsePreprocessor.java | 22 +++ .../PatternReplacingContentModifier.java} | 30 ++-- .../operation/preprocess/Preprocessors.java | 122 ++++++++++++++++ .../PrettyPrintingContentModifier.java} | 37 ++--- .../ContentModifyingReponsePostProcessor.java | 96 ------------- .../HeaderRemovingResponsePostProcessor.java | 134 ------------------ .../response/ResponsePostProcessors.java | 91 ------------ .../restdocs/ResponseModifierTests.java | 59 -------- .../RestDocumentationIntegrationTests.java | 77 +++++++--- ...ntModifyingOperationPreprocessorTests.java | 79 +++++++++++ ...derRemovingOperationPreprocessorTests.java | 62 ++++++++ .../LinkMaskingContentModifierTests.java} | 54 +++---- .../PatternReplacingContentModifierTests.java | 31 ++++ .../PrettyPrintingContentModifierTests.java} | 28 ++-- ...ntModifyingResponsePostProcessorTests.java | 61 -------- ...derRemovingResponsePostProcessorTests.java | 91 ------------ ...rnReplacingResponsePostProcessorTests.java | 47 ------ 34 files changed, 991 insertions(+), 884 deletions(-) create mode 100644 docs/src/docs/asciidoc/customizing-requests-and-responses.adoc delete mode 100644 docs/src/docs/asciidoc/customizing-responses.adoc rename docs/src/test/java/com/example/{ResponsePostProcessing.java => Preprocessing.java} (63%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java rename spring-restdocs/src/main/java/org/springframework/restdocs/{response/ResponsePostProcessor.java => operation/preprocess/ContentModifier.java} (50%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java rename spring-restdocs/src/main/java/org/springframework/restdocs/{response/LinkMaskingResponsePostProcessor.java => operation/preprocess/LinkMaskingContentModifier.java} (61%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java rename spring-restdocs/src/main/java/org/springframework/restdocs/{response/PatternReplacingResponsePostProcessor.java => operation/preprocess/PatternReplacingContentModifier.java} (54%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java rename spring-restdocs/src/main/java/org/springframework/restdocs/{response/PrettyPrintingResponsePostProcessor.java => operation/preprocess/PrettyPrintingContentModifier.java} (73%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{response/LinkMaskingResponsePostProcessorTests.java => operation/preprocess/LinkMaskingContentModifierTests.java} (65%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{response/PrettyPrintingResponsePostProcessorTests.java => operation/preprocess/PrettyPrintingContentModifierTests.java} (57%) delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc new file mode 100644 index 000000000..524f3afa8 --- /dev/null +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -0,0 +1,56 @@ +[[customizing-requests-and-responses]] +== Customizing requests and responses + +There may be situations where you do not want to document a request exactly as it was sent +or a response exactly as it was received. Spring REST Docs provides a number of +preprocessors that can be used to modify a request or response before it's documented. + +Preprocessing is configured by calling `document` with an `OperationRequestPreprocessor`, +and/or an `OperationResponsePreprocessor`. Instances can be obtained using the +static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Preprocessing.java[tags=general] +---- +<1> Apply a request preprocessor that will remove the header named `Foo`. +<2> Apply a response preprocessor that will pretty print its content. + +Various built in preprocessors, including those illustrated above, are available via the +static methods on `Preprocessors`. See below for further details. + + + +[[customizing-requests-and-responses-pretty-printing]] +=== Pretty printing + +`prettyPrint` on `Preprocessors` formats the content of the request or response +to make it easier to read. + + + +[[customizing-requests-and-responses-masking-links]] +=== Masking links + +If you're documenting a Hypermedia-based API, you may want to encourage clients to +navigate the API using links rather than through the use of hard coded URIs. One way to do +this is to limit the use of URIs in the documentation. `maskLinks` on +`Preprocessors` replaces the `href` of any links in the response with `...`. A +different replacement can also be specified if you wish. + + + +[[customizing-requests-and-responses-removing-headers]] +=== Removing headers + +`removeHeaders` on `Preprocessors` removes any occurrences of the named headers +from the request or response. + + + +[[customizing-requests-and-responses-replacing-patterns]] +=== Replacing patterns + +`replacePattern` on `Preprocessors` provides a general purpose mechanism for +replacing content in a request or response. Any occurrences of a regular expression are +replaced. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/customizing-responses.adoc b/docs/src/docs/asciidoc/customizing-responses.adoc deleted file mode 100644 index fe2eb415f..000000000 --- a/docs/src/docs/asciidoc/customizing-responses.adoc +++ /dev/null @@ -1,45 +0,0 @@ -[[customizing-responses]] -== Customizing responses - -There may be situations where you do not want to document a response exactly as received. -Spring REST Docs provides a number of response post processors that can be used to modify -a response after it is received but before it's documented. - -Response modification is configured using a `ResponseModifier`. An instance can be -obtained using the static `modifyResponseTo` method on `RestDocumentation`. Once the -response modifications have been provided, documentation can be configured as usual -via the `andDocument` method: - -[source,java,indent=0] ----- -include::{examples-dir}/com/example/ResponsePostProcessing.java[tags=general] ----- -<1> Call `modifyResponseTo` to configure response modifications, passing in one or more -`ResponsePostProcessor` implementations. -<2> Proceed with documenting the call. - - -[[customizing-responses-pretty-printing]] -=== Pretty printing - -`prettyPrintContent` on `ResponsePostProcessors` formats the body of the response to -make it easier to read. - -[[customizing-responses-masking-links]] -=== Masking links - -If you're documenting a Hypermedia-based API, you may want to encourage clients to -navigate the API using links rather than through the use of hard coded URIs. One way to do -this is to limit the use of URIs in the documentation. `maskLinks` on -`ResponsePostProcessors` replaces the `href` of any links in the response with `...`. A -different replacement can also be specified if you wish. - -=== Removing headers - -`removeHeaders` on `ResponsePostProcessors` removes any occurrences of the named headers -from the response. - -=== Replacing patterns - -`replacePattern` on `ResponsePostProcessors` provides a general purpose mechanism for -replacing content in a response. Any occurrences of a regular expression are replaced. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 2fb04b36e..d4821b024 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -23,7 +23,7 @@ snippets produced with Spring MVC Test. include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] -include::customizing-responses.adoc[] +include::customizing-requests-and-responses.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/test/java/com/example/ResponsePostProcessing.java b/docs/src/test/java/com/example/Preprocessing.java similarity index 63% rename from docs/src/test/java/com/example/ResponsePostProcessing.java rename to docs/src/test/java/com/example/Preprocessing.java index a0c8d8ae0..cf2d9ad27 100644 --- a/docs/src/test/java/com/example/ResponsePostProcessing.java +++ b/docs/src/test/java/com/example/Preprocessing.java @@ -16,13 +16,17 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.RestDocumentation.document; import org.springframework.test.web.servlet.MockMvc; -public class ResponsePostProcessing { +public class Preprocessing { private MockMvc mockMvc; @@ -30,8 +34,9 @@ public void general() throws Exception { // tag::general[] this.mockMvc.perform(get("/")) .andExpect(status().isOk()) - .andDo(modifyResponseTo(/* ... */) // <1> - .andDocument("index")); // <2> + .andDo(document("index", + preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> // end::general[] } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java deleted file mode 100644 index a72ded801..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java +++ /dev/null @@ -1,123 +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; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; - -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.cglib.proxy.MethodInterceptor; -import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.response.ResponsePostProcessor; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.ReflectionUtils; - -/** - * Modifies the response in an {@link MvcResult} by applying {@link ResponsePostProcessor - * ResponsePostProcessors} to it. - * - * @see RestDocumentation#modifyResponseTo(ResponsePostProcessor...) - * @author Andy Wilkinson - */ -public final class ResponseModifier { - - private final List postProcessors; - - ResponseModifier(ResponsePostProcessor... postProcessors) { - this.postProcessors = Arrays.asList(postProcessors); - } - - /** - * Provides a {@link RestDocumentationResultHandler} that can be used to document the - * request and modified response. - * @param identifier an identifier for the API call that is being documented - * @param snippets the snippets to use to document the call - * @return the result handler that will produce the documentation - */ - public RestDocumentationResultHandler andDocument(String identifier, - Snippet... snippets) { - return new ResponseModifyingRestDocumentationResultHandler(identifier, snippets); - } - - class ResponseModifyingRestDocumentationResultHandler extends - RestDocumentationResultHandler { - - private ResponseModifyingRestDocumentationResultHandler(String identifier, - Snippet... snippets) { - super(identifier, snippets); - } - - @Override - public void handle(MvcResult result) throws Exception { - super.handle(postProcessResponse(result)); - } - - MvcResult postProcessResponse(MvcResult result) throws Exception { - MockHttpServletResponse response = result.getResponse(); - for (ResponsePostProcessor postProcessor : ResponseModifier.this.postProcessors) { - response = postProcessor.postProcess(response); - } - return decorateResult(result, response); - } - - private MvcResult decorateResult(MvcResult result, - MockHttpServletResponse response) { - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(MvcResult.class); - enhancer.setCallback(new GetResponseMethodInterceptor(response, result)); - return (MvcResult) enhancer.create(); - } - - private class GetResponseMethodInterceptor implements MethodInterceptor { - - private final MvcResult delegate; - - private final MockHttpServletResponse response; - - private final Method getResponseMethod = findMethod("getResponse"); - - private GetResponseMethodInterceptor(MockHttpServletResponse response, - MvcResult delegate) { - this.delegate = delegate; - this.response = response; - } - - @Override - public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws IllegalAccessException, - InvocationTargetException { - if (this.getResponseMethod.equals(method)) { - return this.response; - } - return method.invoke(this.delegate, args); - } - - private Method findMethod(String methodName) { - return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod( - MvcResult.class, methodName)); - } - - } - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index d5977b3c9..952ca4230 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -17,11 +17,10 @@ package org.springframework.restdocs; import org.springframework.restdocs.config.RestDocumentationConfigurer; -import org.springframework.restdocs.response.ResponsePostProcessor; -import org.springframework.restdocs.response.ResponsePostProcessors; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -50,7 +49,7 @@ public static RestDocumentationConfigurer documentationConfiguration() { /** * Documents the API call with the given {@code identifier} using the given - * {@code handlers}. + * {@code snippets}. * * @param identifier an identifier for the API call that is being documented * @param snippets the snippets that will document the API call @@ -64,17 +63,60 @@ public static RestDocumentationResultHandler document(String identifier, } /** - * Enables the modification of the response in a {@link MvcResult} prior to it being - * documented. The modification is performed using the given - * {@code responsePostProcessors}. + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. The given {@code requestPreprocessor} is applied to the request + * before it is documented. * - * @param responsePostProcessors the post-processors to use to modify the response - * @return the response modifier - * @see ResponsePostProcessors + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets that will document the API call + * @return a Mock MVC {@code ResultHandler} that will produce the documentation + * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) + * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) */ - public static ResponseModifier modifyResponseTo( - ResponsePostProcessor... responsePostProcessors) { - return new ResponseModifier(responsePostProcessors); + public static RestDocumentationResultHandler document(String identifier, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + return new RestDocumentationResultHandler(identifier, requestPreprocessor, + snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. The given {@code responsePreprocessor} is applied to the request + * before it is documented. + * + * @param identifier an identifier for the API call that is being documented + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets that will document the API call + * @return a Mock MVC {@code ResultHandler} that will produce the documentation + * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) + * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) + */ + public static RestDocumentationResultHandler document(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationResultHandler(identifier, responsePreprocessor, + snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. The given {@code requestPreprocessor} and + * {@code responsePreprocessor} are applied to the request and response respectively + * before they are documented. + * + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets that will document the API call + * @return a Mock MVC {@code ResultHandler} that will produce the documentation + * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) + * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) + */ + public static RestDocumentationResultHandler document(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationResultHandler(identifier, requestPreprocessor, + responsePreprocessor, snippets); } } 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 71bca2b5a..e1fc99f11 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -26,10 +26,16 @@ import org.springframework.restdocs.operation.MockMvcOperationRequestFactory; import org.springframework.restdocs.operation.MockMvcOperationResponseFactory; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.util.Assert; /** * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. @@ -42,10 +48,39 @@ public class RestDocumentationResultHandler implements ResultHandler { private final String identifier; + private final OperationRequestPreprocessor requestPreprocessor; + + private final OperationResponsePreprocessor responsePreprocessor; + private final List snippets; RestDocumentationResultHandler(String identifier, Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationResultHandler(String identifier, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + this(identifier, requestPreprocessor, + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationResultHandler(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + responsePreprocessor, snippets); + } + + RestDocumentationResultHandler(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + Assert.notNull(identifier, "identifier must be non-null"); + Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null"); + Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null"); + Assert.notNull(snippets, "snippets must be non-null"); this.identifier = identifier; + this.requestPreprocessor = requestPreprocessor; + this.responsePreprocessor = responsePreprocessor; this.snippets = Arrays.asList(snippets); } @@ -55,11 +90,15 @@ public void handle(MvcResult result) throws Exception { for (String name : iterable(result.getRequest().getAttributeNames())) { attributes.put(name, result.getRequest().getAttribute(name)); } - StandardOperation operation = new StandardOperation(this.identifier, - new MockMvcOperationRequestFactory().createOperationRequest(result - .getRequest()), - new MockMvcOperationResponseFactory().createOperationResponse(result - .getResponse()), attributes); + OperationRequest request = this.requestPreprocessor + .preprocess(new MockMvcOperationRequestFactory() + .createOperationRequest(result.getRequest())); + + OperationResponse response = this.responsePreprocessor + .preprocess(new MockMvcOperationResponseFactory() + .createOperationResponse(result.getResponse())); + Operation operation = new StandardOperation(this.identifier, request, response, + attributes); for (Snippet snippet : getSnippets(result)) { snippet.document(operation); } @@ -74,4 +113,24 @@ private List getSnippets(MvcResult result) { return combinedSnippets; } + static final class IdentityOperationRequestPreprocessor implements + OperationRequestPreprocessor { + + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + } + + static final class IdentityOperationResponsePreprocessor implements + OperationResponsePreprocessor { + + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 37ed936f7..b4bac39c9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -59,7 +59,7 @@ public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, Collection parts) { this.uri = uri; this.method = method; - this.content = content == null ? new byte[0] : content; + this.content = content; this.headers = headers; this.parameters = parameters; this.parts = parts; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index f7a1c5948..c8fcf401e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -44,7 +44,7 @@ public StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) { this.status = status; this.headers = headers; - this.content = content == null ? new byte[0] : content; + this.content = content; } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java similarity index 50% rename from spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java index c685626f7..52e1075df 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java @@ -14,26 +14,27 @@ * limitations under the License. */ -package org.springframework.restdocs.response; +package org.springframework.restdocs.operation.preprocess; -import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; /** - * A {@code ResponsePostProcessor} is used to modify the response received from a MockMvc - * call prior to the response being documented. + * A {@code ContentModifier} modifies the content of an {@link OperationRequest} or + * {@link OperationResponse} during the preprocessing that is performed prior to + * documentation generation. * * @author Andy Wilkinson + * @see ContentModifyingOperationPreprocessor */ -public interface ResponsePostProcessor { +interface ContentModifier { /** - * Post-processes the given {@code response}, returning a, possibly new, - * {@link MockHttpServletResponse} that should now be used. + * Returns modified content based on the given {@code originalContent} * - * @param response The response to post-process - * @return The result of the post-processing - * @throws Exception if a failure occurs during the post-processing + * @param originalContent the original content + * @return the modified content */ - MockHttpServletResponse postProcess(MockHttpServletResponse response) - throws Exception; + byte[] modifyContent(byte[] originalContent); + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java new file mode 100644 index 000000000..1ff1e8191 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -0,0 +1,65 @@ +/* + * 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.operation.preprocess; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationResponse; + +/** + * An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content + * of the request or response. + * + * @author Andy Wilkinson + */ +class ContentModifyingOperationPreprocessor implements OperationPreprocessor { + + private final ContentModifier contentModifier; + + ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { + this.contentModifier = contentModifier; + } + + @Override + public OperationRequest preprocess(OperationRequest request) { + byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent()); + return new StandardOperationRequest(request.getUri(), request.getMethod(), + modifiedContent, + getUpdatedHeaders(request.getHeaders(), modifiedContent), + request.getParameters(), request.getParts()); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + byte[] modifiedContent = this.contentModifier + .modifyContent(response.getContent()); + return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders( + response.getHeaders(), modifiedContent), modifiedContent); + } + + private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) { + HttpHeaders updatedHeaders = new HttpHeaders(); + updatedHeaders.putAll(headers); + if (updatedHeaders.getContentLength() > -1) { + updatedHeaders.setContentLength(updatedContent.length); + } + return updatedHeaders; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java new file mode 100644 index 000000000..ee57e83a4 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java @@ -0,0 +1,57 @@ +/* + * 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.operation.preprocess; + +import java.util.List; + +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.util.Assert; + +/** + * An {@link OperationRequestPreprocessor} that delgates to one or more + * {@link OperationPreprocessor OperationPreprocessors} to preprocess an + * {@link OperationRequest}. + * + * @author Andy Wilkinson + * + */ +class DelegatingOperationRequestPreprocessor implements OperationRequestPreprocessor { + + private final List delegates; + + /** + * Creates a new {@code DelegatingOperationRequestPreprocessor} that will delegate to + * the given {@code delegates} by calling + * {@link OperationPreprocessor#preprocess(OperationRequest)}. + * + * @param delegates the delegates + */ + DelegatingOperationRequestPreprocessor(List delegates) { + Assert.notNull(delegates, "delegates must be non-null"); + this.delegates = delegates; + } + + @Override + public OperationRequest preprocess(OperationRequest operationRequest) { + OperationRequest preprocessedRequest = operationRequest; + for (OperationPreprocessor delegate : this.delegates) { + preprocessedRequest = delegate.preprocess(preprocessedRequest); + } + return preprocessedRequest; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java new file mode 100644 index 000000000..e200c25d6 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java @@ -0,0 +1,56 @@ +/* + * 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.operation.preprocess; + +import java.util.List; + +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.util.Assert; + +/** + * An {@link OperationResponsePreprocessor} that delgates to one or more + * {@link OperationPreprocessor OperationPreprocessors} to preprocess an + * {@link OperationResponse}. + * + * @author Andy Wilkinson + */ +class DelegatingOperationResponsePreprocessor implements OperationResponsePreprocessor { + + private final List delegates; + + /** + * Creates a new {@code DelegatingOperationResponsePreprocessor} that will delegate to + * the given {@code delegates} by calling + * {@link OperationPreprocessor#preprocess(OperationResponse)}. + * + * @param delegates the delegates + */ + DelegatingOperationResponsePreprocessor(List delegates) { + Assert.notNull(delegates, "delegates must be non-null"); + this.delegates = delegates; + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + OperationResponse preprocessedResponse = response; + for (OperationPreprocessor delegate : this.delegates) { + preprocessedResponse = delegate.preprocess(preprocessedResponse); + } + return preprocessedResponse; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java new file mode 100644 index 000000000..ef258a15e --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -0,0 +1,63 @@ +/* + * 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.operation.preprocess; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationResponse; + +/** + * An {@link OperationPreprocessor} that removes headers + * + * @author Andy Wilkinson + */ +class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { + + private final Set headersToRemove; + + HeaderRemovingOperationPreprocessor(String... headersToRemove) { + this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + return new StandardOperationResponse(response.getStatus(), + removeHeaders(response.getHeaders()), response.getContent()); + } + + @Override + public OperationRequest preprocess(OperationRequest request) { + return new StandardOperationRequest(request.getUri(), request.getMethod(), + request.getContent(), removeHeaders(request.getHeaders()), + request.getParameters(), request.getParts()); + } + + private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { + HttpHeaders processedHeaders = new HttpHeaders(); + processedHeaders.putAll(originalHeaders); + for (String headerToRemove : this.headersToRemove) { + processedHeaders.remove(headerToRemove); + } + return processedHeaders; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java similarity index 61% rename from spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 8396b1633..74a5ff5f9 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -14,30 +14,35 @@ * limitations under the License. */ -package org.springframework.restdocs.response; +package org.springframework.restdocs.operation.preprocess; import java.util.regex.Pattern; /** - * A {@link ResponsePostProcessor} that modifies the content of a hypermedia response to - * mask the hrefs of any links. + * A content modifier the masks the {@code href} of any hypermedia links * * @author Andy Wilkinson - * @author Dewet Diener */ -class LinkMaskingResponsePostProcessor extends PatternReplacingResponsePostProcessor { +class LinkMaskingContentModifier implements ContentModifier { private static final String DEFAULT_MASK = "..."; private static final Pattern LINK_HREF = Pattern.compile( "\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); - LinkMaskingResponsePostProcessor() { - super(LINK_HREF, DEFAULT_MASK); + private final ContentModifier contentModifier; + + LinkMaskingContentModifier() { + this(DEFAULT_MASK); + } + + LinkMaskingContentModifier(String mask) { + this.contentModifier = new PatternReplacingContentModifier(LINK_HREF, mask); } - LinkMaskingResponsePostProcessor(String mask) { - super(LINK_HREF, mask); + @Override + public byte[] modifyContent(byte[] originalContent) { + return this.contentModifier.modifyContent(originalContent); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java new file mode 100644 index 000000000..e28bcaddf --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java @@ -0,0 +1,47 @@ +/* + * 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.operation.preprocess; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * An {@code OperationPreprocessor} processes the {@link OperationRequest} and + * {@link OperationResponse} of an {@link Operation} prior to it being documented. + * + * @author Andy Wilkinson + */ +public interface OperationPreprocessor { + + /** + * Processes the given {@code request} + * + * @param request the request to process + * @return the processed request + */ + OperationRequest preprocess(OperationRequest request); + + /** + * Processes the given {@code response} + * + * @param response the response to process + * @return the processed response + */ + OperationResponse preprocess(OperationResponse response); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java new file mode 100644 index 000000000..39fd21329 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java @@ -0,0 +1,38 @@ +/* + * 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.operation.preprocess; + +import org.springframework.restdocs.operation.OperationRequest; + +/** + * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} + * prior to it being documented. + * + * @author Andy Wilkinson + */ +public interface OperationRequestPreprocessor { + + /** + * Processes and potentially modifies the given {@code request} before it is + * documented. + * + * @param request the request + * @return the modified request + */ + OperationRequest preprocess(OperationRequest request); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java new file mode 100644 index 000000000..a8ea46fc7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java @@ -0,0 +1,22 @@ +package org.springframework.restdocs.operation.preprocess; + +import org.springframework.restdocs.operation.OperationResponse; + +/** + * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} + * prior to it being documented. + * + * @author Andy Wilkinson + */ +public interface OperationResponsePreprocessor { + + /** + * Processes and potentially modifies the given {@code response} before it is + * documented. + * + * @param response the response + * @return the modified response + */ + OperationResponse preprocess(OperationResponse response); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java similarity index 54% rename from spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index ce6403416..0b702f912 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -14,43 +14,51 @@ * limitations under the License. */ -package org.springframework.restdocs.response; +package org.springframework.restdocs.operation.preprocess; import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * A {@link ResponsePostProcessor} that modifies the content of the response by replacing - * occurrences of a regular expression {@link Pattern}. + * A {@link ContentModifier} that modifies the content by replacing occurrences of a + * regular expression {@link Pattern}. * * @author Andy Wilkinson * @author Dewet Diener */ -class PatternReplacingResponsePostProcessor extends ContentModifyingReponsePostProcessor { +class PatternReplacingContentModifier implements ContentModifier { private final Pattern pattern; private final String replacement; - PatternReplacingResponsePostProcessor(Pattern pattern, String replacement) { + /** + * Creates a new {@link PatternReplacingContentModifier} that will replace occurences + * the given {@code pattern} with the given {@code replacement}. + * + * @param pattern the pattern + * @param replacement the replacement + */ + PatternReplacingContentModifier(Pattern pattern, String replacement) { this.pattern = pattern; this.replacement = replacement; } @Override - protected String modifyContent(String originalContent) { - Matcher matcher = this.pattern.matcher(originalContent); + public byte[] modifyContent(byte[] content) { + String original = new String(content); + Matcher matcher = this.pattern.matcher(original); StringBuilder buffer = new StringBuilder(); int previous = 0; while (matcher.find()) { - buffer.append(originalContent.substring(previous, matcher.start(1))); + buffer.append(original.substring(previous, matcher.start(1))); buffer.append(this.replacement); previous = matcher.end(1); } - if (previous < originalContent.length()) { - buffer.append(originalContent.substring(previous)); + if (previous < original.length()) { + buffer.append(original.substring(previous)); } - return buffer.toString(); + return buffer.toString().getBytes(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java new file mode 100644 index 000000000..563b8b532 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.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.operation.preprocess; + +import java.util.Arrays; +import java.util.regex.Pattern; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * Static factory methods for creating {@link OperationPreprocessor + * OperationPreprocessors} that can be applied to an {@link Operation Operation's} + * {@link OperationRequest request} or {@link OperationResponse response} before it is + * documented. + * + * @author Andy Wilkinson + */ +public class Preprocessors { + + private Preprocessors() { + + } + + /** + * Returns an {@link OperationRequestPreprocessor} that will preprocess the request by + * applying the given {@code preprocessors} to it. + * + * @param preprocessors the preprocessors + * @return the request preprocessor + */ + public static OperationRequestPreprocessor preprocessRequest( + OperationPreprocessor... preprocessors) { + return new DelegatingOperationRequestPreprocessor(Arrays.asList(preprocessors)); + } + + /** + * Returns an {@link OperationResponsePreprocessor} that will preprocess the response + * by applying the given {@code preprocessors} to it. + * + * @param preprocessors the preprocessors + * @return the response preprocessor + */ + public static OperationResponsePreprocessor preprocessResponse( + OperationPreprocessor... preprocessors) { + return new DelegatingOperationResponsePreprocessor(Arrays.asList(preprocessors)); + } + + /** + * Returns an {@code OperationPreprocessor} that will pretty print the content of the + * request or response. + * + * @return the preprocessor + */ + public static OperationPreprocessor prettyPrint() { + return new ContentModifyingOperationPreprocessor( + new PrettyPrintingContentModifier()); + } + + /** + * Returns an {@code OperationPreprocessor} that will remove headers from the request + * or response. + * + * @param headersToRemove the names of the headers to remove + * @return the preprocessor + */ + public static OperationPreprocessor removeHeaders(String... headersToRemove) { + return new HeaderRemovingOperationPreprocessor(headersToRemove); + } + + /** + * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia + * links in the request or response. + * + * @return the preprocessor + */ + public static OperationPreprocessor maskLinks() { + return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier()); + } + + /** + * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia + * links in the request or response. + * + * @param mask the link mask + * @return the preprocessor + */ + public static OperationPreprocessor maskLinks(String mask) { + return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier( + mask)); + } + + /** + * Returns an {@code OperationPreprocessor} that will modify the content of the + * request or response by replacing occurences of the given {@code pattern} with the + * given {@code replacement} + * + * @param pattern the pattern + * @param replacement the replacement + * @return the preprocessor + */ + public static OperationPreprocessor replacePattern(Pattern pattern, String replacement) { + return new ContentModifyingOperationPreprocessor( + new PatternReplacingContentModifier(pattern, replacement)); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java similarity index 73% rename from spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 4e457c803..9e193b3e5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.restdocs.response; +package org.springframework.restdocs.operation.preprocess; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.StringReader; import java.io.StringWriter; import java.util.Arrays; import java.util.Collections; @@ -29,27 +29,28 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; -import org.springframework.util.StringUtils; - import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -class PrettyPrintingResponsePostProcessor extends ContentModifyingReponsePostProcessor { +/** + * A {@link ContentModifier} that modifies the content by pretty printing it. + * + * @author Andy Wilkinson + */ +public class PrettyPrintingContentModifier implements ContentModifier { private static final List PRETTY_PRINTERS = Collections .unmodifiableList(Arrays.asList(new JsonPrettyPrinter(), new XmlPrettyPrinter())); @Override - protected String modifyContent(String originalContent) { - if (StringUtils.hasText(originalContent)) { - for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { - try { - return prettyPrinter.prettyPrint(originalContent); - } - catch (Exception ex) { - // Continue - } + public byte[] modifyContent(byte[] originalContent) { + for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { + try { + return prettyPrinter.prettyPrint(originalContent).getBytes(); + } + catch (Exception ex) { + // Continue } } return originalContent; @@ -57,21 +58,21 @@ protected String modifyContent(String originalContent) { private interface PrettyPrinter { - String prettyPrint(String string) throws Exception; + String prettyPrint(byte[] content) throws Exception; } private static final class XmlPrettyPrinter implements PrettyPrinter { @Override - public String prettyPrint(String original) throws Exception { + public String prettyPrint(byte[] original) throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); StringWriter transformed = new StringWriter(); - transformer.transform(new StreamSource(new StringReader(original)), + transformer.transform(new StreamSource(new ByteArrayInputStream(original)), new StreamResult(transformed)); return transformed.toString(); } @@ -80,7 +81,7 @@ public String prettyPrint(String original) throws Exception { private static final class JsonPrettyPrinter implements PrettyPrinter { @Override - public String prettyPrint(String original) throws IOException { + public String prettyPrint(byte[] original) throws IOException { ObjectMapper objectMapper = new ObjectMapper().configure( SerializationFeature.INDENT_OUTPUT, true); return objectMapper.writeValueAsString(objectMapper.readTree(original)); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java deleted file mode 100644 index 52640b3f4..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java +++ /dev/null @@ -1,96 +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.response; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.cglib.proxy.MethodInterceptor; -import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.util.ReflectionUtils; - -/** - * A base class for {@link ResponsePostProcessor ResponsePostProcessors} that modify the - * content of the response. - * - * @author Andy Wilkinson - */ -public abstract class ContentModifyingReponsePostProcessor implements - ResponsePostProcessor { - - @Override - public MockHttpServletResponse postProcess(MockHttpServletResponse response) - throws Exception { - String modifiedContent = modifyContent(response.getContentAsString()); - - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(MockHttpServletResponse.class); - enhancer.setCallback(new ContentModifyingMethodInterceptor(modifiedContent, - response)); - - return (MockHttpServletResponse) enhancer.create(); - } - - /** - * Returns a modified version of the given {@code originalContent} - * - * @param originalContent the content to modify - * @return the modified content - * @throws Exception if a failure occurs while modifying the content - */ - protected abstract String modifyContent(String originalContent) throws Exception; - - private static class ContentModifyingMethodInterceptor implements MethodInterceptor { - - private final Method getContentAsStringMethod = findMethod("getContentAsString"); - - private final Method getContentAsByteArray = findMethod("getContentAsByteArray"); - - private final String modifiedContent; - - private final MockHttpServletResponse delegate; - - private ContentModifyingMethodInterceptor(String modifiedContent, - MockHttpServletResponse delegate) { - this.modifiedContent = modifiedContent; - this.delegate = delegate; - } - - @Override - public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws IllegalAccessException, - InvocationTargetException { - if (this.getContentAsStringMethod.equals(method)) { - return this.modifiedContent; - } - if (this.getContentAsByteArray.equals(method)) { - return this.modifiedContent.getBytes(); - } - return method.invoke(this.delegate, args); - } - - private static Method findMethod(String methodName) { - return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod( - MockHttpServletResponse.class, methodName)); - } - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java deleted file mode 100644 index 53da363e5..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java +++ /dev/null @@ -1,134 +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.response; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.cglib.proxy.MethodInterceptor; -import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.util.ReflectionUtils; - -/** - * A {@link ResponsePostProcessor} that removes headers from the response - * - * @author Andy Wilkinson - */ -class HeaderRemovingResponsePostProcessor implements ResponsePostProcessor { - - private final Set headersToRemove; - - HeaderRemovingResponsePostProcessor(String... headersToRemove) { - this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); - } - - @Override - public MockHttpServletResponse postProcess(final MockHttpServletResponse response) { - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(MockHttpServletResponse.class); - enhancer.setCallback(new HeaderHidingMethodInterceptor(this.headersToRemove, - response)); - - return (MockHttpServletResponse) enhancer.create(); - } - - private static final class HeaderHidingMethodInterceptor implements MethodInterceptor { - - private final MockHttpServletResponse response; - - private final List interceptedMethods = Arrays.asList( - findHeaderMethod("containsHeader", String.class), - findHeaderMethod("getHeader", String.class), - findHeaderMethod("getHeaderValue", String.class), - findHeaderMethod("getHeaders", String.class), - findHeaderMethod("getHeaderValues", String.class)); - - private final Method getHeaderNamesMethod = findHeaderMethod("getHeaderNames"); - - private final Set hiddenHeaders; - - private HeaderHidingMethodInterceptor(Set hiddenHeaders, - MockHttpServletResponse response) { - this.hiddenHeaders = hiddenHeaders; - this.response = response; - } - - @Override - public Object intercept(Object proxy, Method method, Object[] args, - MethodProxy methodProxy) throws IllegalAccessException, - InvocationTargetException { - if (this.getHeaderNamesMethod.equals(method)) { - List headerNames = new ArrayList<>(); - for (String candidate : this.response.getHeaderNames()) { - if (!isHiddenHeader(candidate)) { - headerNames.add(candidate); - } - } - return headerNames; - } - if (this.interceptedMethods.contains(method) && isHiddenHeader(args)) { - if (method.getReturnType().equals(boolean.class)) { - return false; - } - else if (Collection.class.isAssignableFrom(method.getReturnType())) { - return Collections.emptyList(); - } - else { - return null; - } - } - - return method.invoke(this.response, args); - } - - private boolean isHiddenHeader(Object[] args) { - if (args.length == 1 && args[0] instanceof String) { - return isHiddenHeader((String) args[0]); - } - return false; - } - - private boolean isHiddenHeader(String headerName) { - for (String hiddenHeader : this.hiddenHeaders) { - if (hiddenHeader.equalsIgnoreCase(headerName)) { - return true; - } - } - return false; - } - - private static Method findHeaderMethod(String methodName, Class... args) { - Method candidate = ReflectionUtils.findMethod(MockHttpServletResponse.class, - methodName, args); - if (candidate.isBridge()) { - return BridgeMethodResolver.findBridgedMethod(candidate); - } - return candidate; - } - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java deleted file mode 100644 index 6c1f22952..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java +++ /dev/null @@ -1,91 +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.response; - -import java.util.regex.Pattern; - -/** - * Static factory methods for accessing various {@link ResponsePostProcessor - * ResponsePostProcessors}. - * - * @author Andy Wilkinson - * @author Dewet Diener - */ -public abstract class ResponsePostProcessors { - - private ResponsePostProcessors() { - - } - - /** - * Returns a {@link ResponsePostProcessor} that will pretty print the content of the - * response. - * - * @return the response post-processor - */ - public static ResponsePostProcessor prettyPrintContent() { - return new PrettyPrintingResponsePostProcessor(); - } - - /** - * Returns a {@link ResponsePostProcessor} that will remove the headers with the given - * {@code headerNames} from the response. - * - * @param headerNames the name of the headers to remove - * @return the response post-processor - */ - public static ResponsePostProcessor removeHeaders(String... headerNames) { - return new HeaderRemovingResponsePostProcessor(headerNames); - } - - /** - * Returns a {@link ResponsePostProcessor} that will update the content of the - * response to mask any links that it contains. Each link is masked my replacing its - * {@code href} with {@code ...}. - * - * @return the response post-processor - */ - public static ResponsePostProcessor maskLinks() { - return new LinkMaskingResponsePostProcessor(); - } - - /** - * Returns a {@link ResponsePostProcessor} that will update the content of the - * response to mask any links that it contains. Each link is masked my replacing its - * {@code href} with the given {@code mask}. - * - * @param mask the mask to apply - * @return the response post-processor - */ - public static ResponsePostProcessor maskLinksWith(String mask) { - return new LinkMaskingResponsePostProcessor(mask); - } - - /** - * Returns a {@link ResponsePostProcessor} that will update the content of the - * response by replacing any occurrences of the given {@code pattern} with the given - * {@code replacement}. - * - * @param pattern the pattern to match - * @param replacement the replacement to apply - * @return the response post-processor - */ - public static ResponsePostProcessor replacePattern(Pattern pattern, String replacement) { - return new PatternReplacingResponsePostProcessor(pattern, replacement); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java deleted file mode 100644 index 63a3862c8..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java +++ /dev/null @@ -1,59 +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; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.test.StubMvcResult.result; - -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.ResponseModifier.ResponseModifyingRestDocumentationResultHandler; -import org.springframework.restdocs.response.ResponsePostProcessor; - -/** - * Tests for {@link ResponseModifier} - * - * @author Andy Wilkinson - * - */ -public class ResponseModifierTests { - - @Test - public void postProcessorsAreApplied() throws Exception { - ResponsePostProcessor first = mock(ResponsePostProcessor.class); - ResponsePostProcessor second = mock(ResponsePostProcessor.class); - - MockHttpServletResponse original = new MockHttpServletResponse(); - MockHttpServletResponse afterFirst = new MockHttpServletResponse(); - MockHttpServletResponse afterSecond = new MockHttpServletResponse(); - - given(first.postProcess(original)).willReturn(afterFirst); - given(second.postProcess(afterFirst)).willReturn(afterSecond); - - RestDocumentationResultHandler resultHandler = new ResponseModifier(first, second) - .andDocument("test"); - assertThat( - afterSecond, - is(equalTo(((ResponseModifyingRestDocumentationResultHandler) resultHandler) - .postProcessResponse(result(original)).getResponse()))); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 194d0bd7b..bd7c08bf5 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -21,23 +21,25 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; -import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; -import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; -import static org.springframework.restdocs.response.ResponsePostProcessors.replacePattern; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -71,6 +73,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.FileSystemUtils; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -236,15 +239,62 @@ public void multiStep() throws Exception { } @Test - public void postProcessedResponse() throws Exception { + public void preprocessedRequest() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfigurer()).build(); + Pattern pattern = Pattern.compile("(\"alpha\")"); + + mockMvc.perform( + get("/").header("a", "alpha").header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) + .andExpect(status().isOk()) + .andDo(document("original-request")) + .andDo(document( + "preprocessed-request", + preprocessRequest(prettyPrint(), removeHeaders("a"), + replacePattern(pattern, "\"<>\"")))); + + assertThat( + new File("build/generated-snippets/original-request/http-request.adoc"), + is(snippet().withContents( + httpRequest(RequestMethod.GET, "/").header("Host", "localhost") + .header("a", "alpha").header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); + assertThat( + new File( + "build/generated-snippets/preprocessed-request/http-request.adoc"), + is(snippet().withContents( + httpRequest(RequestMethod.GET, "/").header("Host", "localhost") + .header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Length", "22") + .content(String.format("{%n \"a\" : \"<>\"%n}"))))); + } + + @Test + public void preprocessedResponse() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + Pattern pattern = Pattern.compile("(\"alpha\")"); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("original")); + .andExpect(status().isOk()) + .andDo(document("original-response")) + .andDo(document( + "preprocessed-response", + preprocessResponse(prettyPrint(), maskLinks(), + removeHeaders("a"), + replacePattern(pattern, "\"<>\"")))); assertThat( - new File("build/generated-snippets/original/http-response.adoc"), + new File("build/generated-snippets/original-response/http-response.adoc"), is(snippet().withContents( httpResponse(HttpStatus.OK) .header("a", "alpha") @@ -252,16 +302,9 @@ public void postProcessedResponse() throws Exception { .content( "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + "\"href\":\"href\"}]}")))); - - Pattern pattern = Pattern.compile("(\"alpha\")"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"), - replacePattern(pattern, "\"<>\""), maskLinks()) - .andDocument("post-processed")); - assertThat( - new File("build/generated-snippets/post-processed/http-response.adoc"), + new File( + "build/generated-snippets/preprocessed-response/http-response.adoc"), is(snippet().withContents( httpResponse(HttpStatus.OK).header("Content-Type", "application/json").content( diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..0a31fd8f9 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -0,0 +1,79 @@ +package org.springframework.restdocs.operation.preprocess; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationResponse; + +/** + * Tests for {@link ContentModifyingOperationPreprocessor} + * + * @author Andy Wilkinson + * + */ +public class ContentModifyingOperationPreprocessorTests { + + private final ContentModifyingOperationPreprocessor preprocessor = new ContentModifyingOperationPreprocessor( + new ContentModifier() { + + @Override + public byte[] modifyContent(byte[] originalContent) { + return "modified".getBytes(); + } + + }); + + @Test + public void modifyRequestContent() { + StandardOperationRequest request = new StandardOperationRequest( + URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), + new HttpHeaders(), new Parameters(), + Collections. emptyList()); + OperationRequest preprocessed = this.preprocessor.preprocess(request); + assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); + } + + @Test + public void modifyResponseContent() { + StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + new HttpHeaders(), "content".getBytes()); + OperationResponse preprocessed = this.preprocessor.preprocess(response); + assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); + } + + @Test + public void unknownContentLengthIsUnchanged() { + StandardOperationRequest request = new StandardOperationRequest( + URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), + new HttpHeaders(), new Parameters(), + Collections. emptyList()); + OperationRequest preprocessed = this.preprocessor.preprocess(request); + assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(-1L))); + } + + @Test + public void contentLengthIsUpdated() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentLength(7); + StandardOperationRequest request = new StandardOperationRequest( + URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), + httpHeaders, new Parameters(), + Collections. emptyList()); + OperationRequest preprocessed = this.preprocessor.preprocess(request); + assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(8L))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java new file mode 100644 index 000000000..c90f4afdc --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -0,0 +1,62 @@ +package org.springframework.restdocs.operation.preprocess; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationResponse; + +/** + * Tests for {@link HeaderRemovingOperationPreprocessorTests} + * + * @author Andy Wilkinson + * + */ +public class HeaderRemovingOperationPreprocessorTests { + + private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( + "b"); + + @Test + public void modifyRequestHeaders() { + StandardOperationRequest request = new StandardOperationRequest( + URI.create("http://localhost"), HttpMethod.GET, new byte[0], + getHttpHeaders(), new Parameters(), + Collections. emptyList()); + OperationRequest preprocessed = this.preprocessor.preprocess(request); + assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); + assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + } + + @Test + public void modifyResponseHeaders() { + StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + getHttpHeaders(), new byte[0]); + OperationResponse preprocessed = this.preprocessor.preprocess(response); + assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); + assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + } + + private HttpHeaders getHttpHeaders() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("a", "alpha"); + httpHeaders.add("b", "bravo"); + httpHeaders.add("b", "banana"); + return httpHeaders; + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java similarity index 65% rename from spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index ddc66d454..2763d6465 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -1,20 +1,4 @@ -/* - * 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.response; +package org.springframework.restdocs.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -34,9 +18,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -public class LinkMaskingResponsePostProcessorTests { +/** + * Tests for {@link LinkMaskingContentModifier} + * + * @author Andy Wilkinson + * + */ +public class LinkMaskingContentModifierTests { - private final LinkMaskingResponsePostProcessor postProcessor = new LinkMaskingResponsePostProcessor(); + private final ContentModifier contentModifier = new LinkMaskingContentModifier(); private final Link[] links = new Link[] { new Link("a", "alpha"), new Link("b", "bravo") }; @@ -46,28 +36,28 @@ public class LinkMaskingResponsePostProcessorTests { @Test public void halLinksAreMasked() throws Exception { - assertThat(this.postProcessor.modifyContent(halPayloadWithLinks(this.links)), + assertThat(this.contentModifier.modifyContent(halPayloadWithLinks(this.links)), is(equalTo(halPayloadWithLinks(this.maskedLinks)))); } @Test public void formattedHalLinksAreMasked() throws Exception { assertThat( - this.postProcessor + this.contentModifier .modifyContent(formattedHalPayloadWithLinks(this.links)), is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); } @Test public void atomLinksAreMasked() throws Exception { - assertThat(this.postProcessor.modifyContent(atomPayloadWithLinks(this.links)), + assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links)), is(equalTo(atomPayloadWithLinks(this.maskedLinks)))); } @Test public void formattedAtomLinksAreMasked() throws Exception { assertThat( - this.postProcessor + this.contentModifier .modifyContent(formattedAtomPayloadWithLinks(this.links)), is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); } @@ -75,20 +65,20 @@ public void formattedAtomLinksAreMasked() throws Exception { @Test public void maskCanBeCustomized() throws Exception { assertThat( - new LinkMaskingResponsePostProcessor("custom") + new LinkMaskingContentModifier("custom") .modifyContent(formattedAtomPayloadWithLinks(this.links)), is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))))); } - private String atomPayloadWithLinks(Link... links) throws JsonProcessingException { - return new ObjectMapper().writeValueAsString(createAtomPayload(links)); + private byte[] atomPayloadWithLinks(Link... links) throws JsonProcessingException { + return new ObjectMapper().writeValueAsBytes(createAtomPayload(links)); } - private String formattedAtomPayloadWithLinks(Link... links) + private byte[] formattedAtomPayloadWithLinks(Link... links) throws JsonProcessingException { return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) - .writeValueAsString(createAtomPayload(links)); + .writeValueAsBytes(createAtomPayload(links)); } private AtomPayload createAtomPayload(Link... links) { @@ -97,14 +87,14 @@ private AtomPayload createAtomPayload(Link... links) { return payload; } - private String halPayloadWithLinks(Link... links) throws JsonProcessingException { - return new ObjectMapper().writeValueAsString(createHalPayload(links)); + private byte[] halPayloadWithLinks(Link... links) throws JsonProcessingException { + return new ObjectMapper().writeValueAsBytes(createHalPayload(links)); } - private String formattedHalPayloadWithLinks(Link... links) + private byte[] formattedHalPayloadWithLinks(Link... links) throws JsonProcessingException { return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) - .writeValueAsString(createHalPayload(links)); + .writeValueAsBytes(createHalPayload(links)); } private HalPayload createHalPayload(Link... links) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java new file mode 100644 index 000000000..f419296d6 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -0,0 +1,31 @@ +package org.springframework.restdocs.operation.preprocess; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * Tests for {@link PatternReplacingContentModifier} + * + * @author Andy Wilkinson + * + */ +public class PatternReplacingContentModifierTests { + + @Test + public void patternsAreReplaced() throws Exception { + Pattern pattern = Pattern.compile( + "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", + Pattern.CASE_INSENSITIVE); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( + pattern, "<>"); + assertThat( + contentModifier.modifyContent("{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}" + .getBytes()), is(equalTo("{\"id\" : \"<>\"}".getBytes()))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java similarity index 57% rename from spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 5a5b45775..8e6aa197d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.response; +package org.springframework.restdocs.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; @@ -22,38 +22,40 @@ import org.junit.Test; /** - * Tests for {@link PrettyPrintingResponsePostProcessor} + * Tests for {@link PrettyPrintingContentModifier} * * @author Andy Wilkinson * */ -public class PrettyPrintingResponsePostProcessorTests { +public class PrettyPrintingContentModifierTests { @Test public void prettyPrintJson() throws Exception { - assertThat(new PrettyPrintingResponsePostProcessor().modifyContent("{\"a\":5}"), - equalTo(String.format("{%n \"a\" : 5%n}"))); + assertThat( + new PrettyPrintingContentModifier().modifyContent("{\"a\":5}".getBytes()), + equalTo(String.format("{%n \"a\" : 5%n}").getBytes())); } @Test public void prettyPrintXml() throws Exception { assertThat( - new PrettyPrintingResponsePostProcessor() - .modifyContent(""), - equalTo(String.format("%n" - + "%n %n%n"))); + new PrettyPrintingContentModifier().modifyContent("" + .getBytes()), equalTo(String.format( + "%n" + + "%n %n%n") + .getBytes())); } @Test public void empytContentIsHandledGracefully() throws Exception { - assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(""), - equalTo("")); + assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes()), + equalTo("".getBytes())); } @Test public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { String content = "abcdefg"; - assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(content), - equalTo(content)); + assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes()), + equalTo(content.getBytes())); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java deleted file mode 100644 index 09487a3d9..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java +++ /dev/null @@ -1,61 +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.response; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.mock.web.MockHttpServletResponse; - -public class ContentModifyingResponsePostProcessorTests { - - private final MockHttpServletResponse original = new MockHttpServletResponse(); - - private final ContentModifyingReponsePostProcessor postProcessor = new TestContentModifyingResponsePostProcessor(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void contentCanBeModified() throws Exception { - MockHttpServletResponse modified = this.postProcessor.postProcess(this.original); - assertThat(modified.getContentAsString(), is(equalTo("modified"))); - assertThat(modified.getContentAsByteArray(), is(equalTo("modified".getBytes()))); - } - - @Test - public void nonContentMethodsAreDelegated() throws Exception { - this.original.addHeader("a", "alpha"); - MockHttpServletResponse modified = this.postProcessor.postProcess(this.original); - assertThat(modified.getHeader("a"), is(equalTo("alpha"))); - } - - private static final class TestContentModifyingResponsePostProcessor extends - ContentModifyingReponsePostProcessor { - - @Override - protected String modifyContent(String originalContent) throws Exception { - return "modified"; - } - - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java deleted file mode 100644 index 1891eadc0..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java +++ /dev/null @@ -1,91 +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.response; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.empty; -import static org.junit.Assert.assertThat; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; - -/** - * Tests for {@link HeaderRemovingResponsePostProcessor}. - * - * @author Andy Wilkinson - * - */ -public class HeaderRemovingResponsePostProcessorTests { - - private final MockHttpServletResponse response = new MockHttpServletResponse(); - - @Before - public void configureResponse() { - this.response.addHeader("a", "alpha"); - this.response.addHeader("b", "bravo"); - } - - @Test - public void containsHeaderHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.containsHeader("a"), is(false)); - assertThat(response.containsHeader("b"), is(true)); - } - - @Test - public void getHeaderNamesHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.getHeaderNames(), contains("b")); - } - - @Test - public void getHeaderHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.getHeader("a"), is(nullValue())); - assertThat(response.getHeader("b"), is("bravo")); - } - - @Test - public void getHeadersHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.getHeaders("a"), is(empty())); - assertThat(response.getHeaders("b"), contains("bravo")); - } - - @Test - public void getHeaderValueHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.getHeaderValue("a"), is(nullValue())); - assertThat(response.getHeaderValue("b"), is((Object) "bravo")); - } - - @Test - public void getHeaderValuesHonoursRemovedHeaders() { - MockHttpServletResponse response = removeHeaders("a"); - assertThat(response.getHeaderValues("a"), is(empty())); - assertThat(response.getHeaderValues("b"), contains((Object) "bravo")); - } - - private MockHttpServletResponse removeHeaders(String... headerNames) { - return new HeaderRemovingResponsePostProcessor(headerNames) - .postProcess(this.response); - } - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java deleted file mode 100644 index 1074ea352..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PatternReplacingResponsePostProcessorTests.java +++ /dev/null @@ -1,47 +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.response; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -import java.util.regex.Pattern; - -import org.junit.Test; - -/** - * Tests for {@link PatternReplacingResponsePostProcessor}. - * - * @author Dewet Diener - */ -public class PatternReplacingResponsePostProcessorTests { - - @Test - public void patternsAreReplaced() throws Exception { - Pattern pattern = Pattern.compile( - "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", - Pattern.CASE_INSENSITIVE); - PatternReplacingResponsePostProcessor postProcessor = new PatternReplacingResponsePostProcessor( - pattern, "<>"); - assertThat( - postProcessor - .modifyContent("{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}"), - is(equalTo("{\"id\" : \"<>\"}"))); - } - -} From 2b2b6fcd252d3df3af15c3b92e921e7b6b71c631 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 2 Sep 2015 16:20:34 +0100 Subject: [PATCH 0120/1059] Isolate and reduce Spring Test dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit splits Spring REST Docs into two projects – spring-restdocs-core and spring-restdocs-mockmvc. spring-restdocs-core contains the vast majority of the code but does not depend on a specific test framework other than JUnit. The use of a Spring Test TestExecutionListener has been replaced with a JUnit test rule. The rule is declared once per test class and configured with the output directory to which the generated snippets should be written. This simplifies the implementation as thread local storage is no longer required to transfer information about the test that’s running into Spring REST Docs. Instead, this transfer is now handled by the new test rule. It has also simplified the configuration as it’s no longer necessary for users to provide a system property that configures the output directory. spring-restdocs-mockmvc contains code that’s specific to using Spring REST Docs with Spring MVC Test’s MockMvc. This is currently the only testing framework that’s supported, but it paves the way for adding support for additional frameworks. REST Assured is one that users seem particularly interested in (see gh-80 and gh-102). Closes gh-107 --- build.gradle | 109 +++++++++++- .../build/SampleBuildConfigurer.groovy | 13 +- docs/build.gradle | 6 +- docs/src/docs/asciidoc/configuration.adoc | 20 --- docs/src/docs/asciidoc/getting-started.adoc | 68 ++++---- docs/src/test/java/com/example/AlwaysDo.java | 13 +- .../test/java/com/example/Constraints.java | 24 ++- .../CustomDefaultSnippetsConfiguration.java | 25 ++- .../test/java/com/example/CustomEncoding.java | 9 +- .../com/example/CustomUriConfiguration.java | 9 +- .../com/example/ExampleApplicationTests.java | 11 +- .../src/test/java/com/example/Hypermedia.java | 4 +- .../test/java/com/example/InvokeService.java | 4 +- .../test/java/com/example/PathParameters.java | 4 +- docs/src/test/java/com/example/Payload.java | 6 +- .../test/java/com/example/Preprocessing.java | 4 +- .../java/com/example/RequestParameters.java | 6 +- .../rest-notes-spring-data-rest/build.gradle | 68 -------- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - samples/rest-notes-spring-data-rest/gradlew | 164 ------------------ .../rest-notes-spring-data-rest/gradlew.bat | 90 ---------- samples/rest-notes-spring-data-rest/pom.xml | 9 +- .../com/example/notes/ApiDocumentation.java | 30 ++-- .../notes/GettingStartedDocumentation.java | 17 +- .../rest-notes-spring-hateoas/build.gradle | 7 +- samples/rest-notes-spring-hateoas/pom.xml | 137 --------------- .../com/example/notes/ApiDocumentation.java | 17 +- .../notes/GettingStartedDocumentation.java | 17 +- settings.gradle | 5 +- .../.settings/org.eclipse.jdt.core.prefs | 0 .../.settings/org.eclipse.jdt.ui.prefs | 0 spring-restdocs-core/build.gradle | 59 +++++++ .../restdocs/RestDocumentation.java | 81 +++++++++ .../restdocs/RestDocumentationContext.java | 99 +++++++++++ .../restdocs/constraints/Constraint.java | 0 .../ConstraintDescriptionResolver.java | 0 .../constraints/ConstraintDescriptions.java | 0 .../constraints/ConstraintResolver.java | 0 ...ceBundleConstraintDescriptionResolver.java | 0 .../ValidatorConstraintResolver.java | 0 .../restdocs/curl/CurlDocumentation.java | 0 .../restdocs/curl/CurlRequestSnippet.java | 0 .../restdocs/http/HttpDocumentation.java | 0 .../restdocs/http/HttpRequestSnippet.java | 0 .../restdocs/http/HttpResponseSnippet.java | 0 .../hypermedia/AbstractJsonLinkExtractor.java | 0 .../hypermedia/AtomLinkExtractor.java | 0 .../hypermedia/ContentTypeLinkExtractor.java | 18 +- .../restdocs/hypermedia/HalLinkExtractor.java | 0 .../hypermedia/HypermediaDocumentation.java | 0 .../restdocs/hypermedia/Link.java | 0 .../restdocs/hypermedia/LinkDescriptor.java | 0 .../restdocs/hypermedia/LinkExtractor.java | 0 .../restdocs/hypermedia/LinksSnippet.java | 0 .../restdocs/operation/Operation.java | 0 .../restdocs/operation/OperationRequest.java | 0 .../operation/OperationRequestPart.java | 0 .../restdocs/operation/OperationResponse.java | 0 .../restdocs/operation/Parameters.java | 0 .../restdocs/operation/StandardOperation.java | 0 .../operation/StandardOperationRequest.java | 0 .../StandardOperationRequestPart.java | 0 .../operation/StandardOperationResponse.java | 0 .../operation/preprocess/ContentModifier.java | 0 ...ContentModifyingOperationPreprocessor.java | 0 ...elegatingOperationRequestPreprocessor.java | 0 ...legatingOperationResponsePreprocessor.java | 0 .../HeaderRemovingOperationPreprocessor.java | 0 .../LinkMaskingContentModifier.java | 0 .../preprocess/OperationPreprocessor.java | 0 .../OperationRequestPreprocessor.java | 0 .../OperationResponsePreprocessor.java | 38 ++++ .../PatternReplacingContentModifier.java | 0 .../operation/preprocess/Preprocessors.java | 0 .../PrettyPrintingContentModifier.java | 0 .../payload/AbstractFieldsSnippet.java | 0 .../restdocs/payload/ContentHandler.java | 0 .../restdocs/payload/FieldDescriptor.java | 0 .../payload/FieldDoesNotExistException.java | 0 .../payload/FieldTypeRequiredException.java | 0 .../restdocs/payload/JsonContentHandler.java | 18 +- .../restdocs/payload/JsonFieldPath.java | 0 .../restdocs/payload/JsonFieldProcessor.java | 0 .../restdocs/payload/JsonFieldType.java | 0 .../payload/JsonFieldTypeResolver.java | 0 .../payload/PayloadDocumentation.java | 0 .../payload/PayloadHandlingException.java | 38 ++-- .../payload/RequestFieldsSnippet.java | 0 .../payload/ResponseFieldsSnippet.java | 0 .../restdocs/payload/XmlContentHandler.java | 0 .../request/AbstractParametersSnippet.java | 0 .../restdocs/request/ParameterDescriptor.java | 0 .../request/PathParametersSnippet.java | 0 .../request/RequestDocumentation.java | 0 .../request/RequestParametersSnippet.java | 0 .../restdocs/snippet/AbstractDescriptor.java | 22 ++- .../restdocs/snippet/Attributes.java | 0 ...cumentationContextPlaceholderResolver.java | 21 +-- .../restdocs/snippet/Snippet.java | 0 .../restdocs/snippet/SnippetException.java | 0 .../snippet/StandardWriterResolver.java | 16 +- .../restdocs/snippet/TemplatedSnippet.java | 7 +- .../restdocs/snippet/WriterResolver.java | 6 +- .../StandardTemplateResourceResolver.java | 0 .../restdocs/templates/Template.java | 38 ++++ .../restdocs/templates/TemplateEngine.java | 0 .../templates/TemplateResourceResolver.java | 0 .../templates/mustache/MustacheTemplate.java | 0 .../mustache/MustacheTemplateEngine.java | 0 .../DefaultConstraintDescriptions.properties | 0 .../templates/default-curl-request.snippet | 0 .../templates/default-http-request.snippet | 0 .../templates/default-http-response.snippet | 0 .../restdocs/templates/default-links.snippet | 0 .../templates/default-path-parameters.snippet | 0 .../templates/default-request-fields.snippet | 0 .../default-request-parameters.snippet | 0 .../templates/default-response-fields.snippet | 0 .../ConstraintDescriptionsTests.java | 18 +- ...dleConstraintDescriptionResolverTests.java | 18 +- .../ValidatorConstraintResolverTests.java | 18 +- .../curl/CurlRequestSnippetTests.java | 80 +++++---- .../http/HttpRequestSnippetTests.java | 52 +++--- .../http/HttpResponseSnippetTests.java | 24 +-- .../ContentTypeLinkExtractorTests.java | 0 .../LinkExtractorsPayloadTests.java | 0 .../hypermedia/LinksSnippetTests.java | 33 ++-- ...ntModifyingOperationPreprocessorTests.java | 18 +- ...tingOperationRequestPreprocessorTests.java | 60 +++++++ ...ingOperationResponsePreprocessorTests.java | 61 +++++++ ...derRemovingOperationPreprocessorTests.java | 18 +- .../LinkMaskingContentModifierTests.java | 18 +- .../PatternReplacingContentModifierTests.java | 18 +- .../PrettyPrintingContentModifierTests.java | 0 .../restdocs/payload/JsonFieldPathTests.java | 0 .../payload/JsonFieldProcessorTests.java | 0 .../payload/JsonFieldTypeResolverTests.java | 0 .../payload/RequestFieldsSnippetTests.java | 49 ++++-- .../payload/ResponseFieldsSnippetTests.java | 45 +++-- .../request/PathParametersSnippetTests.java | 31 ++-- .../RequestParametersSnippetTests.java | 24 +-- ...tationContextPlaceholderResolverTests.java | 65 +++++++ .../snippet/StandardWriterResolverTests.java | 34 ++-- .../restdocs/test/ExpectedSnippet.java | 40 ++--- .../restdocs/test/OperationBuilder.java | 29 +++- .../restdocs/test/SnippetMatchers.java | 0 .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 .../request-fields-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...ultiple-fields-and-embedded-and-links.json | 0 .../multiple-fields-and-embedded.json | 0 .../multiple-fields-and-links.json | 0 .../field-payloads/multiple-fields.json | 0 .../resources/field-payloads/no-fields.json | 0 .../field-payloads/single-field.json | 0 .../atom/multiple-links-different-rels.json | 0 .../atom/multiple-links-same-rels.json | 0 .../link-payloads/atom/no-links.json | 0 .../link-payloads/atom/single-link.json | 0 .../link-payloads/atom/wrong-format.json | 0 .../hal/multiple-links-different-rels.json | 0 .../hal/multiple-links-same-rels.json | 0 .../resources/link-payloads/hal/no-links.json | 0 .../link-payloads/hal/single-link.json | 0 .../link-payloads/hal/wrong-format.json | 0 .../TestConstraintDescriptions.properties | 0 spring-restdocs-mockmvc/build.gradle | 14 ++ .../restdocs/mockmvc}/AbstractConfigurer.java | 2 +- .../mockmvc}/AbstractNestedConfigurer.java | 2 +- .../MockMvcOperationRequestFactory.java | 11 +- .../MockMvcOperationResponseFactory.java | 6 +- .../mockmvc/MockMvcRestDocumentation.java | 20 ++- .../restdocs/mockmvc}/NestedConfigurer.java | 6 +- .../RestDocumentationMockMvcConfigurer.java | 53 +++--- .../RestDocumentationRequestBuilders.java | 2 +- .../RestDocumentationResultHandler.java | 8 +- .../restdocs/mockmvc}/SnippetConfigurer.java | 7 +- .../restdocs/mockmvc}/UriConfigurer.java | 6 +- .../mockmvc}/util/IterableEnumeration.java | 2 +- .../MockMvcOperationRequestFactoryTests.java | 22 ++- ...kMvcRestDocumentationIntegrationTests.java | 46 ++--- .../RestDocumentationConfigurerTests.java | 49 +++--- ...RestDocumentationRequestBuildersTests.java | 21 +-- .../restdocs/mockmvc}/test/StubMvcResult.java | 8 +- .../mockmvc}/test/TestRequestBuilders.java | 8 +- .../restdocs/templates/curl-request.snippet | 0 spring-restdocs/build.gradle | 116 ------------- .../config/RestDocumentationContext.java | 73 -------- ...estDocumentationTestExecutionListener.java | 41 ----- .../OperationResponsePreprocessor.java | 22 --- .../payload/PayloadHandlingException.java | 20 --- .../snippet/DocumentationProperties.java | 52 ------ .../restdocs/templates/Template.java | 22 --- .../main/resources/META-INF/spring.factories | 1 - .../config/RestDocumentationContexts.java | 6 - ...tationContextPlaceholderResolverTests.java | 61 ------- 207 files changed, 1397 insertions(+), 1426 deletions(-) delete mode 100644 samples/rest-notes-spring-data-rest/build.gradle delete mode 100644 samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/rest-notes-spring-data-rest/gradlew delete mode 100644 samples/rest-notes-spring-data-rest/gradlew.bat delete mode 100644 samples/rest-notes-spring-hateoas/pom.xml rename {spring-restdocs => spring-restdocs-core}/.settings/org.eclipse.jdt.core.prefs (100%) rename {spring-restdocs => spring-restdocs-core}/.settings/org.eclipse.jdt.ui.prefs (100%) create mode 100644 spring-restdocs-core/build.gradle create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/Constraint.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java (73%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/Link.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/Operation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/OperationRequest.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/OperationResponse.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/Parameters.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/StandardOperation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java (100%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/ContentHandler.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java (80%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java (100%) rename spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java => spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java (50%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java (60%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/Attributes.java (100%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet}/RestDocumentationContextPlaceholderResolver.java (81%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/Snippet.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/SnippetException.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java (85%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java (86%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java (84%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java (100%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-links.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java (87%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java (91%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java (86%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java (80%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java (85%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java (86%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java (90%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java (82%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java (78%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java (86%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java (58%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java (87%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java (87%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java (87%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java (87%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java (65%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java (77%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/test/OperationBuilder.java (86%) rename {spring-restdocs => spring-restdocs-core}/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/http-request-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/http-response-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/links-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/multiple-fields-and-embedded.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/multiple-fields-and-links.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/multiple-fields.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/no-fields.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/field-payloads/single-field.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/atom/multiple-links-different-rels.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/atom/multiple-links-same-rels.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/atom/no-links.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/atom/single-link.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/atom/wrong-format.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/hal/multiple-links-different-rels.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/hal/multiple-links-same-rels.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/hal/no-links.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/hal/single-link.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/link-payloads/hal/wrong-format.json (100%) rename {spring-restdocs => spring-restdocs-core}/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties (100%) create mode 100644 spring-restdocs-mockmvc/build.gradle rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/AbstractConfigurer.java (95%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/AbstractNestedConfigurer.java (97%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/operation => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/MockMvcOperationRequestFactory.java (91%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/operation => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/MockMvcOperationResponseFactory.java (88%) rename spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java (90%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/NestedConfigurer.java (86%) rename spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java (79%) rename {spring-restdocs/src/main/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/RestDocumentationRequestBuilders.java (99%) rename {spring-restdocs/src/main/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/RestDocumentationResultHandler.java (93%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/SnippetConfigurer.java (93%) rename {spring-restdocs/src/main/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/UriConfigurer.java (93%) rename {spring-restdocs/src/main/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc}/util/IterableEnumeration.java (97%) rename {spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc}/MockMvcOperationRequestFactoryTests.java (92%) rename spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java (90%) rename {spring-restdocs/src/test/java/org/springframework/restdocs/config => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc}/RestDocumentationConfigurerTests.java (73%) rename {spring-restdocs/src/test/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc}/RestDocumentationRequestBuildersTests.java (80%) rename {spring-restdocs/src/test/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc}/test/StubMvcResult.java (95%) rename {spring-restdocs/src/test/java/org/springframework/restdocs => spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc}/test/TestRequestBuilders.java (79%) rename {spring-restdocs => spring-restdocs-mockmvc}/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet (100%) delete mode 100644 spring-restdocs/build.gradle delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java delete mode 100644 spring-restdocs/src/main/resources/META-INF/spring.factories delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java diff --git a/build.gradle b/build.gradle index ee1baef93..d2fb8d363 100644 --- a/build.gradle +++ b/build.gradle @@ -11,9 +11,33 @@ buildscript { allprojects { group = 'org.springframework.restdocs' + repositories { + jcenter() + } } apply plugin: 'samples' +apply plugin: 'sonar-runner' + +sonarRunner { + sonarProperties { + property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" + property 'sonar.java.coveragePlugin', 'jacoco' + property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' + property 'sonar.links.homepage', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.issue', 'https://github.com/spring-projects/spring-restdocs' + property 'sonar.links.scm', 'https://github.com/spring-projects/spring-restdocs' + } +} + +ext { + springVersion = '4.1.7.RELEASE' + javadocLinks = [ + 'http://docs.oracle.com/javase/8/docs/api/', + "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", + 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/' + ] as String[] +} subprojects { apply plugin: 'io.spring.dependency-management' @@ -22,10 +46,14 @@ subprojects { apply plugin: 'propdeps' apply plugin: 'propdeps-eclipse' apply plugin: 'propdeps-maven' + apply plugin: 'maven' + + sourceCompatibility = 1.7 + targetCompatibility = 1.7 dependencyManagement { imports { - mavenBom 'org.springframework:spring-framework-bom:4.1.7.RELEASE' + mavenBom "org.springframework:spring-framework-bom:$springVersion" } dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' @@ -41,10 +69,53 @@ subprojects { dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644' } } + + configurations { + jacoco + } + + dependencies { + jacoco 'org.jacoco:org.jacoco.agent::runtime' + } + + test { + testLogging { + exceptionFormat "full" + } + } + + javadoc { + description = "Generates project-level javadoc for use in -javadoc jar" + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = "Spring REST Docs $version" + options.docTitle = "${options.header} API" + options.links = javadocLinks + options.addStringOption '-quiet' + } + + eclipseJdt.onlyIf { false } + cleanEclipseJdt.onlyIf { false } + + task sourcesJar(type: Jar) { + classifier = 'sources' + from project.sourceSets.main.allSource + } + + task javadocJar(type: Jar) { + classifier = "javadoc" + from javadoc + } + + artifacts { + archives sourcesJar + archives javadocJar + } } samples { - dependOn 'spring-restdocs:install' + dependOn 'spring-restdocs-core:install' + dependOn 'spring-restdocs-mockmvc:install' restNotesSpringHateoas { workingDir 'samples/rest-notes-spring-hateoas' @@ -55,18 +126,44 @@ samples { } } -task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':spring-restdocs:javadoc']) { +task api (type: Javadoc) { + group = "Documentation" + description = "Generates aggregated Javadoc API documentation." + dependsOn { + subprojects.collect { + it.tasks.getByName("jar") + } + } + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = "Spring REST Docs $version" + options.splitIndex = true + options.links = javadocLinks + options.addStringOption '-quiet' + + source subprojects.collect { project -> + project.sourceSets.main.allJava + } + + destinationDir = new File(buildDir, "api") + + doFirst { + classpath = files(subprojects.collect { it.sourceSets.main.compileClasspath }) + } +} + +task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api']) { group = 'Distribution' baseName = 'spring-restdocs' classifier = 'docs' - description = 'Builds -${classifier} archive containing API and reference documentation' + description = "Builds -${classifier} archive containing API and reference documentation" destinationDir = file("${project.buildDir}/distributions") - from (project.tasks.findByPath(':docs:asciidoctor')) { + from(project.tasks.findByPath(':docs:asciidoctor')) { into 'reference' } - from (project.tasks.findByPath(':spring-restdocs:javadoc')) { + from(api) { into 'api' } } diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7bf9293fa..7551ecf2c 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -37,14 +37,19 @@ public class SampleBuildConfigurer { } Task createTask(Project project, Object... dependencies) { - Task mavenBuild = mavenBuild(project, dependencies) - Task gradleBuild = gradleBuild(project, dependencies) Task verifyIncludes = verifyIncludes(project) - verifyIncludes.dependsOn mavenBuild, gradleBuild + if (new File(this.workingDir, 'build.gradle').isFile()) { + Task gradleBuild = gradleBuild(project, dependencies) + verifyIncludes.dependsOn gradleBuild + } + if (new File(this.workingDir, 'pom.xml').isFile()) { + Task mavenBuild = mavenBuild(project, dependencies) + verifyIncludes.dependsOn(mavenBuild) + } Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" - sampleBuild.dependsOn mavenBuild, gradleBuild, verifyIncludes + sampleBuild.dependsOn verifyIncludes return sampleBuild } diff --git a/docs/build.gradle b/docs/build.gradle index b3184452f..5241018c0 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -3,12 +3,8 @@ plugins { } dependencies { - compile 'org.springframework:spring-webmvc' - - testCompile project(':spring-restdocs') + testCompile project(':spring-restdocs-mockmvc') testCompile 'javax.validation:validation-api' - testCompile 'junit:junit' - testCompile 'org.springframework:spring-test' } tasks.findByPath("artifactoryPublish")?.enabled = false diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index e7a0a3757..acd0fd2f4 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -64,24 +64,4 @@ by default: [source,java,indent=0] ---- include::{examples-dir}/com/example/CustomDefaultSnippetsConfiguration.java[tags=custom-default-snippets] ----- - - - -[[configuration-output-directory]] -=== Snippet output directory - -As described in <> the snippet output directory is -configured in your `pom.xml` or `build.gradle` file. This configuration applies to builds -on the command line, but it may not apply when running your tests in your IDE. In the -absence of the property, Spring REST Docs will write the generated snippets to standard -out. - -If you'd prefer that your IDE writes the snippets to disk you can use a file in -`src/test/resources` named `documentation.properties` to specify the output directory that -should be used: - -[source,properties] ----- -org.springframework.restdocs.outputDir: target/generated-snippets ---- \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index f07f9da60..c013ce47d 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -36,10 +36,9 @@ The first step in using Spring REST Docs is to configure your project's build. [[getting-started-build-configuration-gradle]] ==== Gradle build configuration -Both {samples}[sample applications] contain `build.gradle` files that you may wish to -use as a reference. The key parts of the configuration are described below. - - +The {samples/rest-notes-spring-hateoas}[Spring HATEOAS sample] contains a `build.gradle` +file that you may wish to use as a reference. The key parts of the configuration are +described below. [source,groovy,indent=0,subs="verbatim,attributes"] ---- @@ -48,7 +47,7 @@ use as a reference. The key parts of the configuration are described below. } dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs:{project-version}' + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' } ext { <3> @@ -56,24 +55,25 @@ use as a reference. The key parts of the configuration are described below. } test { <4> - systemProperty 'org.springframework.restdocs.outputDir', snippetsDir outputs.dir snippetsDir } asciidoctor { <5> - attributes 'snippets': snippetsDir - inputs.dir snippetsDir - dependsOn test + attributes 'snippets': snippetsDir <6> + inputs.dir snippetsDir <7> + dependsOn test <8> } ---- <1> Apply the Asciidoctor plugin. <2> Add a dependency on spring-restdocs in the `testCompile` configuration. <3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task with the `org.springframework.restdocs.outputDir` system -property. This property controls the location into which Spring REST Docs will write the -snippets that it generates. -<5> Configure the `asciidoctor` task and define an attribute named `snippets`. You can -then use this attribute when including the generated snippets in your documentation. +<4> Configure the `test` task to add the snippets directory as an output. +<5> Configure the `asciidoctor` task +<6> Define an attribute named `snippets` that can be used when including the generated + snippets in your documentation. +<7> Configure the snippets directory as an input. +<8> Make the task depend on the test task so that the tests are run before the + documentation is created. [[getting-started-build-configuration-gradle-packaging-the-documentation]] @@ -100,16 +100,15 @@ directory: [[getting-started-build-configuration-maven]] ==== Maven build configuration -Both {samples}[sample applications] contain `pom.xml` files that you may wish to -use as a reference. The key parts of the configuration are described below. - - +The {samples/rest-notes-spring-data-rest}[Spring Data REST sample] contains a `pom.xml` +file that you may wish to use as a reference. The key parts of the configuration are +described below. [source,xml,indent=0,subs="verbatim,attributes"] ---- <1> org.springframework.restdocs - spring-restdocs + spring-restdocs-mockmvc {project-version} test @@ -127,11 +126,6 @@ use as a reference. The key parts of the configuration are described below. **/*Documentation.java - - - ${snippetsDirectory} - - <4> @@ -159,14 +153,12 @@ use as a reference. The key parts of the configuration are described below. ---- -<1> Add a dependency on `spring-restdocs` in the `test` scope. +<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. <2> Configure a property to define the output location for generated snippets. -<3> Configure the SureFire plugin with the `org.springframework.restdocs.outputDir` system -property. This property controls the location into which Spring REST Docs will write the -snippets that it generates. The plugin is also configured to include files whose names end -with `Documentation.java`. -<4> Configure the Asciidoctor plugin and define an attribute named `snippets`. You can -then use this attribute when including the generated snippets in your documentation. +<3> Add the SureFire plugin and configure it to include files whose names end with + `Documentation.java`. +<4> Add the Asciidoctor plugin and configure it to define an attribute named `snippets` + that can be used when including the generated snippets in your documentation. <5> [[getting-started-build-configuration-maven-plugin-phase]] If you want to <> in your project's jar you should use the `prepare-package` phase. @@ -233,7 +225,8 @@ documentation snippets for the result's request and response. [[getting-started-documentation-snippets-setup]] ==== Setting up Spring MVC test -The first step in generating documentation snippets is to provide an `@Before` method +The first step in generating documentation snippets is to declare a `public` +`RestDocumentation` that's annotated as a JUnit `@Rule` and to provide an `@Before` method that creates a `MockMvc` instance: [source,java,indent=0] @@ -241,6 +234,10 @@ that creates a `MockMvc` instance: include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] ---- +The `RestDocumentation` rule is configured with the output directory into which +generated snippets should be written. This output directory should match the snippets +directory that you have configured in your `build.gradle` or `pom.xml` file. + The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An instance of this class can be obtained from the static `documentationConfiguration()` method on `org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies @@ -259,13 +256,14 @@ service and document the request and response. ---- include::{examples-dir}/com/example/InvokeService.java[tags=invoke-service] ---- -<1> Invoke the root (`/`) of the service an indicate that an `application/json` response +<1> Invoke the root (`/`) of the service and indicate that an `application/json` response is required. -<2> Assert that the service is produced the expected response. +<2> Assert that the service produced the expected response. <3> Document the call to the service, writing the snippets into a directory named `index` that will be located beneath the configured output directory. The snippets are written by a `RestDocumentationResultHandler`. An instance of this class can be obtained from the -static `document` method on `org.springframework.restdocs.RestDocumentation`. +static `document` method on +`org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. By default, three snippets a written: diff --git a/docs/src/test/java/com/example/AlwaysDo.java b/docs/src/test/java/com/example/AlwaysDo.java index d4575b9a7..efc64f6c7 100644 --- a/docs/src/test/java/com/example/AlwaysDo.java +++ b/docs/src/test/java/com/example/AlwaysDo.java @@ -16,25 +16,32 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; public class AlwaysDo { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); private MockMvc mockMvc; private WebApplicationContext context; + + // tag::always-do[] @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()) + .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}/{step}/")) .build(); } diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java index 6370b30cf..5bcea8595 100644 --- a/docs/src/test/java/com/example/Constraints.java +++ b/docs/src/test/java/com/example/Constraints.java @@ -1,3 +1,19 @@ +/* + * 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 com.example; import java.util.List; @@ -15,17 +31,17 @@ public void example() { ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); // <1> List descriptions = userConstraints.descriptionsForProperty("name"); // <2> } - + static class UserInput { - + @NotNull @Size(min = 1) String name; - + @NotNull @Size(min = 8) String password; } // end::constraints[] - + } diff --git a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java b/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java index 0a74af66a..a964c9646 100644 --- a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java +++ b/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java @@ -1,16 +1,37 @@ +/* + * 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 com.example; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import org.junit.Before; +import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; public class CustomDefaultSnippetsConfiguration { + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + @Autowired private WebApplicationContext context; @@ -20,7 +41,7 @@ public class CustomDefaultSnippetsConfiguration { public void setUp() { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration().snippets() + .apply(documentationConfiguration(this.restDocumentation).snippets() .withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] diff --git a/docs/src/test/java/com/example/CustomEncoding.java b/docs/src/test/java/com/example/CustomEncoding.java index 1c72fcff0..4490e8a4d 100644 --- a/docs/src/test/java/com/example/CustomEncoding.java +++ b/docs/src/test/java/com/example/CustomEncoding.java @@ -16,15 +16,20 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import org.junit.Before; +import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; public class CustomEncoding { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); @Autowired private WebApplicationContext context; @@ -35,7 +40,7 @@ public class CustomEncoding { public void setUp() { // tag::custom-encoding[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration().snippets() + .apply(documentationConfiguration(this.restDocumentation).snippets() .withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/CustomUriConfiguration.java index 038d5354b..b70d4e08e 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/CustomUriConfiguration.java @@ -16,15 +16,20 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import org.junit.Before; +import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; public class CustomUriConfiguration { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); @Autowired private WebApplicationContext context; @@ -35,7 +40,7 @@ public class CustomUriConfiguration { public void setUp() { // tag::custom-uri-configuration[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration().uris() + .apply(documentationConfiguration(this.restDocumentation).uris() .withScheme("https") .withHost("example.com") .withPort(443)) diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java index e6c97e444..cc6c64064 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -17,16 +17,21 @@ package com.example; import org.junit.Before; +import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class ExampleApplicationTests { - + // tag::mock-mvc-setup[] + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + @Autowired private WebApplicationContext context; @@ -35,7 +40,7 @@ public class ExampleApplicationTests { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()) + .apply(documentationConfiguration(this.restDocumentation)) .build(); } // end::mock-mvc-setup[] diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java index 103529ea4..3cf6c1bcf 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -16,12 +16,12 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/InvokeService.java index 32fdaf65c..097899f2b 100644 --- a/docs/src/test/java/com/example/InvokeService.java +++ b/docs/src/test/java/com/example/InvokeService.java @@ -16,8 +16,8 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/PathParameters.java index 1754947ec..c78105986 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/PathParameters.java @@ -16,8 +16,8 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 132568a23..5ee1a8902 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -16,9 +16,9 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; diff --git a/docs/src/test/java/com/example/Preprocessing.java b/docs/src/test/java/com/example/Preprocessing.java index cf2d9ad27..dbb837137 100644 --- a/docs/src/test/java/com/example/Preprocessing.java +++ b/docs/src/test/java/com/example/Preprocessing.java @@ -16,13 +16,13 @@ package com.example; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.RestDocumentation.document; import org.springframework.test.web.servlet.MockMvc; diff --git a/docs/src/test/java/com/example/RequestParameters.java b/docs/src/test/java/com/example/RequestParameters.java index 9155a5b9f..5a314958c 100644 --- a/docs/src/test/java/com/example/RequestParameters.java +++ b/docs/src/test/java/com/example/RequestParameters.java @@ -16,9 +16,9 @@ package com.example; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/samples/rest-notes-spring-data-rest/build.gradle b/samples/rest-notes-spring-data-rest/build.gradle deleted file mode 100644 index b30c212be..000000000 --- a/samples/rest-notes-spring-data-rest/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE' - } -} - -plugins { - id "org.asciidoctor.convert" version "1.5.2" -} - -apply plugin: 'java' -apply plugin: 'spring-boot' -apply plugin: 'eclipse' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -dependencies { - compile 'org.springframework.boot:spring-boot-starter-data-rest' - compile 'org.springframework.boot:spring-boot-starter-data-jpa' - - runtime 'com.h2database:h2' - - testCompile 'com.jayway.jsonpath:json-path' - testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs:1.0.0.BUILD-SNAPSHOT' -} - -ext { - snippetsDir = file('build/generated-snippets') -} - -test { - systemProperty 'org.springframework.restdocs.outputDir', snippetsDir - outputs.dir snippetsDir -} - -asciidoctor { - sourceDir 'src/main/asciidoc' // Align with Maven's default location - attributes 'snippets': snippetsDir - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -task wrapper(type: Wrapper) { - gradleVersion = '2.3' -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51017 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 H-~Rf4?cF@p diff --git a/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 72d2eb271..000000000 --- a/samples/rest-notes-spring-data-rest/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Apr 16 12:33:42 BST 2015 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/rest-notes-spring-data-rest/gradlew b/samples/rest-notes-spring-data-rest/gradlew deleted file mode 100755 index 91a7e269e..000000000 --- a/samples/rest-notes-spring-data-rest/gradlew +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-notes-spring-data-rest/gradlew.bat b/samples/rest-notes-spring-data-rest/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/samples/rest-notes-spring-data-rest/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index b553763a4..7e668af49 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,6 @@ UTF-8 1.7 - ${project.build.directory}/generated-snippets @@ -49,7 +48,7 @@
org.springframework.restdocs - spring-restdocs + spring-restdocs-mockmvc 1.0.0.BUILD-SNAPSHOT test @@ -68,9 +67,6 @@ **/*Documentation.java - - ${snippetsDirectory} - @@ -88,7 +84,7 @@ html book - ${snippetsDirectory} + ${project.build.directory}/generated-snippets @@ -96,7 +92,6 @@ maven-resources-plugin - 2.7 copy-resources 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 cfa00bde1..51581811d 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 @@ -18,16 +18,16 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,11 +39,13 @@ import javax.servlet.RequestDispatcher; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -57,6 +59,9 @@ @SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) @WebAppConfiguration public class ApiDocumentation { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); @Autowired private NoteRepository noteRepository; @@ -75,18 +80,7 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()).build(); - this.mockMvc = MockMvcBuilders - .webAppContextSetup(this.context) - .apply(documentationConfiguration() - .uris() - .withScheme("https") - .withHost("localhost") - .withPort(8443) - .and().snippets() - .withEncoding("ISO-8859-1")) - .build(); - + .apply(documentationConfiguration(this.restDocumentation)).build(); } @Test diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index a6afc59fd..3770023ba 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,11 +19,11 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -34,11 +34,13 @@ import java.util.Map; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -54,6 +56,9 @@ @SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) @WebAppConfiguration public class GettingStartedDocumentation { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); @Autowired private ObjectMapper objectMapper; @@ -66,7 +71,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()) + .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}/{step}/")) .build(); } diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 5a2e164db..a8041b902 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -35,7 +35,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs:1.0.0.BUILD-SNAPSHOT' + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:1.0.0.BUILD-SNAPSHOT' } ext { @@ -43,7 +43,6 @@ ext { } test { - systemProperty 'org.springframework.restdocs.outputDir', snippetsDir outputs.dir snippetsDir } @@ -61,9 +60,5 @@ jar { } } -task wrapper(type: Wrapper) { - gradleVersion = '2.3' -} - eclipseJdt.onlyIf { false } cleanEclipseJdt.onlyIf { false } \ No newline at end of file diff --git a/samples/rest-notes-spring-hateoas/pom.xml b/samples/rest-notes-spring-hateoas/pom.xml deleted file mode 100644 index a9a1a3398..000000000 --- a/samples/rest-notes-spring-hateoas/pom.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - 4.0.0 - - com.example - rest-notes-spring-hateoas - 0.0.1-SNAPSHOT - jar - - - org.springframework.boot - spring-boot-starter-parent - 1.2.5.RELEASE - - - - - UTF-8 - 1.7 - ${project.build.directory}/generated-snippets - - - - - org.springframework.boot - spring-boot-starter-hateoas - - - org.springframework.boot - spring-boot-starter-data-jpa - - - com.h2database - h2 - runtime - - - org.atteo - evo-inflector - 1.2 - runtime - - - - org.springframework.boot - spring-boot-starter-test - test - - - com.jayway.jsonpath - json-path - test - - - org.springframework.restdocs - spring-restdocs - 1.0.0.BUILD-SNAPSHOT - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Documentation.java - - - ${snippetsDirectory} - - - - - org.asciidoctor - asciidoctor-maven-plugin - 1.5.2 - - - generate-docs - prepare-package - - process-asciidoc - - - html - book - - ${snippetsDirectory} - - - - - - - maven-resources-plugin - 2.7 - - - copy-resources - prepare-package - - copy-resources - - - ${project.build.outputDirectory}/static/docs - - - ${project.build.directory}/generated-docs - - - - - - - - - - - - spring-snapshots - Spring snapshots - https://repo.spring.io/snapshot - - true - - - - - \ No newline at end of file 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 06e22615b..8a0978df5 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 @@ -18,11 +18,11 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -40,11 +40,13 @@ import javax.servlet.RequestDispatcher; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; @@ -61,6 +63,9 @@ @SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) @WebAppConfiguration public class ApiDocumentation { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); @Autowired private NoteRepository noteRepository; @@ -79,7 +84,7 @@ public class ApiDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()).build(); + .apply(documentationConfiguration(this.restDocumentation)).build(); } @Test diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 79b780515..b13657291 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,11 +19,11 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.RestDocumentation.document; -import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -34,11 +34,13 @@ import java.util.Map; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.RestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -54,6 +56,9 @@ @SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) @WebAppConfiguration public class GettingStartedDocumentation { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); @Autowired private ObjectMapper objectMapper; @@ -66,7 +71,7 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration()) + .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}/{step}/")) .build(); } diff --git a/settings.gradle b/settings.gradle index ad08f6b31..7552e239c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ -rootProject.name = 'spring-restdocs-build' +rootProject.name = 'spring-restdocs' include 'docs' -include 'spring-restdocs' \ No newline at end of file +include 'spring-restdocs-core' +include 'spring-restdocs-mockmvc' \ No newline at end of file diff --git a/spring-restdocs/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from spring-restdocs/.settings/org.eclipse.jdt.core.prefs rename to spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs diff --git a/spring-restdocs/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs similarity index 100% rename from spring-restdocs/.settings/org.eclipse.jdt.ui.prefs rename to spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle new file mode 100644 index 000000000..5f5be34ff --- /dev/null +++ b/spring-restdocs-core/build.gradle @@ -0,0 +1,59 @@ +configurations { + jarjar + jmustache + testArtifacts.extendsFrom testRuntime +} + +task jmustacheRepackJar(type: Jar) { repackJar -> + repackJar.baseName = "restdocs-jmustache-repack" + repackJar.version = dependencyManagement.managedVersions['com.samskivert:jmustache'] + + doLast() { + project.ant { + taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", + classpath: configurations.jarjar.asPath + jarjar(destfile: repackJar.archivePath) { + configurations.jmustache.each { originalJar -> + zipfileset(src: originalJar, includes: '**/*.class') + } + rule(pattern: 'com.samskivert.**', result: 'org.springframework.restdocs.@1') + } + } + } +} + +dependencies { + compile 'com.fasterxml.jackson.core:jackson-databind' + compile 'junit:junit' + compile 'org.springframework:spring-webmvc' + compile 'javax.servlet:javax.servlet-api' + compile files(jmustacheRepackJar) + jarjar 'com.googlecode.jarjar:jarjar:1.3' + jmustache 'com.samskivert:jmustache@jar' + optional 'javax.validation:validation-api' + testCompile 'org.mockito:mockito-core' + testCompile 'org.hamcrest:hamcrest-core' + testCompile 'org.hamcrest:hamcrest-library' + testCompile 'org.hibernate:hibernate-validator' + testRuntime 'org.glassfish:javax.el:3.0.0' +} + +jar { + dependsOn jmustacheRepackJar + from(zipTree(jmustacheRepackJar.archivePath)) { + include "org/springframework/restdocs/**" + } +} + +task testJar(type: Jar) { + classifier "test" + from sourceSets.test.output +} + +artifacts { + testArtifacts testJar +} + +test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*,excludes=org.springframework.restdocs.mustache.*" +} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java new file mode 100644 index 000000000..fc72501e6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -0,0 +1,81 @@ +/* + * 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; + +import java.io.File; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A JUnit {@link TestRule} used to bootstrap the generation of REST documentation from + * JUnit tests. + * + * @author Andy Wilkinson + * + */ +public class RestDocumentation implements TestRule { + + private final String outputDirectory; + + private RestDocumentationContext context; + + /** + * Creates a new {@code RestDocumentation} instance that will generate snippets to the + * given {@code outputDirectory} + * + * @param outputDirectory the output directory + */ + public RestDocumentation(String outputDirectory) { + this.outputDirectory = outputDirectory; + } + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + Class testClass = description.getTestClass(); + String methodName = description.getMethodName(); + RestDocumentation.this.context = new RestDocumentationContext(testClass, + methodName, new File(RestDocumentation.this.outputDirectory)); + try { + base.evaluate(); + } + finally { + RestDocumentation.this.context = null; + } + } + + }; + + } + + /** + * Notification that a RESTful operation that should be documented is about to be + * performed. Returns a {@link RestDocumentationContext} for the operation. + * + * @return the context for the operation + */ + public RestDocumentationContext beforeOperation() { + this.context.getAndIncrementStepCount(); + return this.context; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java new file mode 100644 index 000000000..cad0c71ed --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java @@ -0,0 +1,99 @@ +/* + * 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; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@code RestDocumentationContext} encapsulates the context in which the documentation of + * a RESTful API is being performed. + * + * @author Andy Wilkinson + */ +public final class RestDocumentationContext { + + private final AtomicInteger stepCount = new AtomicInteger(0); + + private final Class testClass; + + private final String testMethodName; + + private final File outputDirectory; + + /** + * Creates a new {@code RestDocumentationContext} for a test on the given + * {@code testClass} with given {@code testMethodName} that will generate + * documentation to the given {@code outputDirectory}. + * + * @param testClass the class whose test is being executed + * @param testMethodName the name of the test method that is being executed + * @param outputDirectory the directory to which documentation should be written. + */ + public RestDocumentationContext(Class testClass, String testMethodName, + File outputDirectory) { + this.testClass = testClass; + this.testMethodName = testMethodName; + this.outputDirectory = outputDirectory; + } + + /** + * Returns the class whose tests are currently executing + * + * @return The test class + */ + public Class getTestClass() { + return this.testClass; + } + + /** + * Returns the name of the test method that is currently executing + * + * @return The name of the test method + */ + public String getTestMethodName() { + return this.testMethodName; + } + + /** + * Returns the current step count and then increments it + * + * @return The step count prior to it being incremented + */ + int getAndIncrementStepCount() { + return this.stepCount.getAndIncrement(); + } + + /** + * Returns the current step count + * + * @return The current step count + */ + public int getStepCount() { + return this.stepCount.get(); + } + + /** + * Returns the output directory to which generated snippets should be written. + * + * @return the output directory + */ + public File getOutputDirectory() { + return this.outputDirectory; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/Constraint.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java similarity index 73% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index 1c31ad3a9..ec36eb63d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -1,3 +1,19 @@ +/* + * 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.hypermedia; import java.io.IOException; @@ -12,7 +28,7 @@ /** * {@link LinkExtractor} that delegates to other link extractors based on the response's * content type. - * + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/Link.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java new file mode 100644 index 000000000..4763b05b3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java @@ -0,0 +1,38 @@ +/* + * 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.operation.preprocess; + +import org.springframework.restdocs.operation.OperationResponse; + +/** + * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} + * prior to it being documented. + * + * @author Andy Wilkinson + */ +public interface OperationResponsePreprocessor { + + /** + * Processes and potentially modifies the given {@code response} before it is + * documented. + * + * @param response the response + * @return the modified response + */ + OperationResponse preprocess(OperationResponse response); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java similarity index 80% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 2fe9e8972..e20b4f8d3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -1,3 +1,19 @@ +/* + * 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; @@ -11,7 +27,7 @@ /** * A {@link ContentHandler} for JSON content - * + * * @author Andy Wilkinson */ class JsonContentHandler implements ContentHandler { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java similarity index 50% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index cefb3fbbc..10362718b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextHolder.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java @@ -14,29 +14,23 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.payload; -import org.springframework.core.NamedInheritableThreadLocal; - -final class RestDocumentationContextHolder { - - private static final NamedInheritableThreadLocal currentContext = new NamedInheritableThreadLocal<>( - "REST Documentation Context"); - - private RestDocumentationContextHolder() { - - } - - static RestDocumentationContext getCurrentContext() { - return currentContext.get(); - } - - static void setCurrentContext(RestDocumentationContext context) { - currentContext.set(context); - } - - static void removeCurrentContext() { - currentContext.remove(); +/** + * Thrown to indicate that a failure has occurred during payload handling + * + * @author Andy Wilkinson + * + */ +@SuppressWarnings("serial") +class PayloadHandlingException extends RuntimeException { + + /** + * Creates a new {@code PayloadHandlingException} with the given cause + * @param cause the cause of the failure + */ + PayloadHandlingException(Throwable cause) { + super(cause); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java similarity index 60% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index d217b002a..4766a171e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -1,3 +1,19 @@ +/* + * 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.snippet; import java.util.HashMap; @@ -8,7 +24,7 @@ /** * Base class for descriptors. Provides the ability to associate arbitrary attributes with * a descriptor. - * + * * @author Andy Wilkinson * * @param the type of the descriptor @@ -19,7 +35,7 @@ public abstract class AbstractDescriptor> { /** * Sets the descriptor's attributes - * + * * @param attributes the attributes * @return the descriptor */ @@ -33,7 +49,7 @@ public T attributes(Attribute... attributes) { /** * Returns the descriptor's attributes - * + * * @return the attributes */ protected Map getAttributes() { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Attributes.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java similarity index 81% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 6a9645cdc..706078d05 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.snippet; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; /** @@ -28,14 +29,14 @@ *
  • {@code step} – the {@link RestDocumentationContext#getStepCount() step current * count}. *
  • {@code methodName} - the name of the - * {@link RestDocumentationContext#getTestMethod() current test method} formatted using - * camelCase + * {@link RestDocumentationContext#getTestMethodName() current test method} formatted + * using camelCase *
  • {@code method-name} - the name of the - * {@link RestDocumentationContext#getTestMethod() current test method} formatted using - * kebab-case + * {@link RestDocumentationContext#getTestMethodName() current test method} formatted + * using kebab-case *
  • {@code method_name} - the name of the - * {@link RestDocumentationContext#getTestMethod() current test method} formatted using - * snake_case + * {@link RestDocumentationContext#getTestMethodName() current test method} formatted + * using snake_case * * * @author Andy Wilkinson @@ -62,13 +63,13 @@ public String resolvePlaceholder(String placeholderName) { return Integer.toString(this.context.getStepCount()); } if ("methodName".equals(placeholderName)) { - return this.context.getTestMethod().getName(); + return this.context.getTestMethodName(); } if ("method-name".equals(placeholderName)) { - return camelCaseToDash(this.context.getTestMethod().getName()); + return camelCaseToDash(this.context.getTestMethodName()); } if ("method_name".equals(placeholderName)) { - return camelCaseToUnderscore(this.context.getTestMethod().getName()); + return camelCaseToUnderscore(this.context.getTestMethodName()); } return null; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetException.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java similarity index 85% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index fd4a52a79..2ba5d366d 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -22,6 +22,7 @@ import java.io.OutputStreamWriter; import java.io.Writer; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -51,9 +52,10 @@ public StandardWriterResolver(PlaceholderResolver placeholderResolver) { } @Override - public Writer resolve(String operationName, String snippetName) throws IOException { + public Writer resolve(String operationName, String snippetName, + RestDocumentationContext context) throws IOException { File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( - operationName, this.placeholderResolver), snippetName + ".adoc"); + operationName, this.placeholderResolver), snippetName + ".adoc", context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); @@ -69,16 +71,18 @@ public void setEncoding(String encoding) { this.encoding = encoding; } - protected File resolveFile(String outputDirectory, String fileName) { + protected File resolveFile(String outputDirectory, String fileName, + RestDocumentationContext context) { File outputFile = new File(outputDirectory, fileName); if (!outputFile.isAbsolute()) { - outputFile = makeRelativeToConfiguredOutputDir(outputFile); + outputFile = makeRelativeToConfiguredOutputDir(outputFile, context); } return outputFile; } - private File makeRelativeToConfiguredOutputDir(File outputFile) { - File configuredOutputDir = new DocumentationProperties().getOutputDir(); + private File makeRelativeToConfiguredOutputDir(File outputFile, + RestDocumentationContext context) { + File configuredOutputDir = context.getOutputDirectory(); if (configuredOutputDir != null) { return new File(configuredOutputDir, outputFile.getPath()); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java similarity index 86% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 7fc188bce..4e0cf65d0 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.templates.Template; import org.springframework.restdocs.templates.TemplateEngine; @@ -46,10 +47,12 @@ protected TemplatedSnippet(String snippetName, Map attributes) { @Override public void document(Operation operation) throws IOException { + RestDocumentationContext context = (RestDocumentationContext) operation + .getAttributes().get(RestDocumentationContext.class.getName()); WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get( WriterResolver.class.getName()); - try (Writer writer = writerResolver - .resolve(operation.getName(), this.snippetName)) { + try (Writer writer = writerResolver.resolve(operation.getName(), + this.snippetName, context)) { Map model = createModel(operation); model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java similarity index 84% rename from spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java index 45948fc5a..9adca5ca7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.io.Writer; +import org.springframework.restdocs.RestDocumentationContext; + /** * A {@code WriterResolver} is used to access the {@link Writer} that should be used to * write a snippet for an operation that is being documented. @@ -32,10 +34,12 @@ public interface WriterResolver { * operation with the given name. * @param operationName the name of the operation that is being documented * @param snippetName the name of the snippet + * @param restDocumentationContext the current documentation context * @return the writer * @throws IOException if a writer cannot be resolved */ - Writer resolve(String operationName, String snippetName) throws IOException; + Writer resolve(String operationName, String snippetName, + RestDocumentationContext restDocumentationContext) throws IOException; /** * Configures the encoding that should be used by any writers produced by this diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java new file mode 100644 index 000000000..7275c5292 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java @@ -0,0 +1,38 @@ +/* + * 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.templates; + +import java.util.Map; + +/** + * A compiled {@code Template} that can be rendered to a {@link String}. + * + * @author Andy Wilkinson + * + */ +public interface Template { + + /** + * Renders the template to a {@link String} using the given {@code context} for + * variable/property resolution. + * + * @param context The context to use + * @return The rendered template + */ + String render(Map context); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java similarity index 100% rename from spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet similarity index 100% rename from spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java similarity index 87% rename from spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 8aa889891..e69192e2a 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -1,3 +1,19 @@ +/* + * 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.constraints; import static org.hamcrest.Matchers.contains; @@ -25,7 +41,7 @@ /** * Tests for {@link ConstraintDescriptions} - * + * * @author Andy Wilkinson */ public class ConstraintDescriptionsTests { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java similarity index 91% rename from spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 03202540e..efb0f2352 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -1,3 +1,19 @@ +/* + * 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.constraints; import static org.hamcrest.CoreMatchers.equalTo; @@ -29,7 +45,7 @@ /** * Tests for {@link ResourceBundleConstraintDescriptionResolver} - * + * * @author Andy Wilkinson */ public class ResourceBundleConstraintDescriptionResolverTests { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java similarity index 86% rename from spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 26ab45cac..e07abd7fe 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -1,3 +1,19 @@ +/* + * 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.constraints; import static org.hamcrest.CoreMatchers.is; @@ -29,7 +45,7 @@ /** * Tests for {@link ValidatorConstraintResolver} - * + * * @author Andy Wilkinson */ public class ValidatorConstraintResolverTests { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java similarity index 80% rename from spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 48a8a5665..96d6927a4 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -31,7 +31,6 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -58,16 +57,18 @@ public class CurlRequestSnippetTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document(new OperationBuilder("get-request").request( - "http://localhost/foo").build()); + new CurlRequestSnippet().document(new OperationBuilder("get-request", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder("non-get-request") - .request("http://localhost/foo").method("POST").build()); + new CurlRequestSnippet().document(new OperationBuilder("non-get-request", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").build()); } @Test @@ -75,8 +76,9 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-content") - .request("http://localhost/foo").content("content").build()); + new CurlRequestSnippet().document(new OperationBuilder("request-with-content", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .content("content").build()); } @Test @@ -86,8 +88,8 @@ public void requestWithQueryString() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); new CurlRequestSnippet().document(new OperationBuilder( - "request-with-query-string").request("http://localhost/foo?param=value") - .build()); + "request-with-query-string", this.snippet.getOutputDirectory()).request( + "http://localhost/foo?param=value").build()); } @Test @@ -95,9 +97,11 @@ public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-one-parameter").request("http://localhost/foo") - .method("POST").param("k1", "v1").build()); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-one-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST").param("k1", "v1") + .build()); } @Test @@ -108,7 +112,8 @@ public void postRequestWithMultipleParameters() throws IOException { "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-multiple-parameters").request("http://localhost/foo") + "post-request-with-multiple-parameters", this.snippet + .getOutputDirectory()).request("http://localhost/foo") .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @@ -120,9 +125,9 @@ public void postRequestWithUrlEncodedParameter() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("POST").param("k1", "a&b") - .build()); + "post-request-with-url-encoded-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test @@ -131,8 +136,8 @@ public void putRequestWithOneParameter() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-one-parameter").request("http://localhost/foo") - .method("PUT").param("k1", "v1").build()); + "put-request-with-one-parameter", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @Test @@ -142,10 +147,11 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-multiple-parameters").request("http://localhost/foo") - .method("PUT").param("k1", "v1").param("k1", "v1-bis").param("k2", "v2") - .build()); + new CurlRequestSnippet() + .document(new OperationBuilder("put-request-with-multiple-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test @@ -155,7 +161,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-url-encoded-parameter").request("http://localhost/foo") + "put-request-with-url-encoded-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo") .method("PUT").param("k1", "a&b").build()); } @@ -165,8 +172,8 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-headers") - .request("http://localhost/foo") + new CurlRequestSnippet().document(new OperationBuilder("request-with-headers", + this.snippet.getOutputDirectory()).request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -179,8 +186,8 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-no-original-filename").request("http://localhost/upload") - .method("POST") + "multipart-post-no-original-filename", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @@ -193,8 +200,8 @@ public void multipartPostWithContentType() throws IOException { this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type").request("http://localhost/upload") - .method("POST") + "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) @@ -208,8 +215,9 @@ public void multipartPost() throws IOException { + "'image=@documents/images/example.png'"; this.snippet.expectCurlRequest("multipart-post").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document(new OperationBuilder("multipart-post", + this.snippet.getOutputDirectory()).request("http://localhost/upload") + .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) .submittedFileName("documents/images/example.png").build()); @@ -224,8 +232,8 @@ public void multipartPostWithParameters() throws IOException { this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters").request("http://localhost/upload") - .method("POST") + "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) .submittedFileName("documents/images/example.png").and() @@ -236,16 +244,14 @@ public void multipartPostWithParameters() throws IOException { public void customAttributes() throws IOException { this.snippet.expectCurlRequest("custom-attributes").withContents( containsString("curl request title")); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("curl-request")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); new CurlRequestSnippet(attributes(key("title").value("curl request title"))) - .document(new OperationBuilder("custom-attributes") + .document(new OperationBuilder("custom-attributes", this.snippet + .getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost/foo").build()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java similarity index 85% rename from spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 3eacda6e0..8da93faab 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -59,8 +59,9 @@ public void getRequest() throws IOException { httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( "Alpha", "a")); - new HttpRequestSnippet().document(new OperationBuilder("get-request") - .request("http://localhost/foo").header("Alpha", "a").build()); + new HttpRequestSnippet().document(new OperationBuilder("get-request", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .header("Alpha", "a").build()); } @Test @@ -69,8 +70,8 @@ public void getRequestWithQueryString() throws IOException { httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); new HttpRequestSnippet().document(new OperationBuilder( - "get-request-with-query-string").request("http://localhost/foo?bar=baz") - .build()); + "get-request-with-query-string", this.snippet.getOutputDirectory()) + .request("http://localhost/foo?bar=baz").build()); } @Test @@ -80,8 +81,9 @@ public void postRequestWithContent() throws IOException { "Hello, world")); new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-content").request("http://localhost/foo") - .method("POST").content("Hello, world").build()); + "post-request-with-content", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST").content("Hello, world") + .build()); } @Test @@ -92,8 +94,9 @@ public void postRequestWithParameter() throws IOException { .content("b%26r=baz&a=alpha")); new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-parameter").request("http://localhost/foo") - .method("POST").param("b&r", "baz").param("a", "alpha").build()); + "post-request-with-parameter", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST").param("b&r", "baz") + .param("a", "alpha").build()); } @Test @@ -102,10 +105,10 @@ public void putRequestWithContent() throws IOException { httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( "Hello, world")); - new HttpRequestSnippet() - .document(new OperationBuilder("put-request-with-content") - .request("http://localhost/foo").method("PUT") - .content("Hello, world").build()); + new HttpRequestSnippet().document(new OperationBuilder( + "put-request-with-content", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("PUT").content("Hello, world") + .build()); } @Test @@ -116,8 +119,9 @@ public void putRequestWithParameter() throws IOException { .content("b%26r=baz&a=alpha")); new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-parameter").request("http://localhost/foo") - .method("PUT").param("b&r", "baz").param("a", "alpha").build()); + "put-request-with-parameter", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("PUT").param("b&r", "baz") + .param("a", "alpha").build()); } @Test @@ -130,8 +134,9 @@ public void multipartPost() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document(new OperationBuilder("multipart-post", + this.snippet.getOutputDirectory()).request("http://localhost/upload") + .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()).build()); } @@ -154,8 +159,8 @@ public void multipartPostWithParameters() throws IOException { "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters").request("http://localhost/upload") - .method("POST") + "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a", "apple", "avocado").param("b", "banana") .part("image", "<< data >>".getBytes()).build()); @@ -173,8 +178,8 @@ public void multipartPostWithContentType() throws IOException { "multipart/form-data; boundary=" + BOUNDARY) .content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type").request("http://localhost/upload") - .method("POST") + "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); @@ -184,8 +189,8 @@ public void multipartPostWithContentType() throws IOException { public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host") - .request("http://localhost/foo") + new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", + this.snippet.getOutputDirectory()).request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com").build()); } @@ -199,7 +204,8 @@ public void requestWithCustomSnippetAttributes() throws IOException { new FileSystemResource( "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) - .document(new OperationBuilder("request-with-snippet-attributes") + .document(new OperationBuilder("request-with-snippet-attributes", + this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost/foo").build()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java similarity index 86% rename from spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 068d6b888..cfe68c60f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -52,16 +52,17 @@ public class HttpResponseSnippetTests { @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); - new HttpResponseSnippet() - .document(new OperationBuilder("basic-response").build()); + new HttpResponseSnippet().document(new OperationBuilder("basic-response", + this.snippet.getOutputDirectory()).build()); } @Test public void nonOkResponse() throws IOException { this.snippet.expectHttpResponse("non-ok-response").withContents( httpResponse(BAD_REQUEST)); - new HttpResponseSnippet().document(new OperationBuilder("non-ok-response") - .response().status(BAD_REQUEST.value()).build()); + new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", + this.snippet.getOutputDirectory()).response().status(BAD_REQUEST.value()) + .build()); } @Test @@ -70,8 +71,8 @@ public void responseWithHeaders() throws IOException { httpResponse(OK) // .header("Content-Type", "application/json") // .header("a", "alpha")); - new HttpResponseSnippet().document(new OperationBuilder("response-with-headers") - .response() + new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", + this.snippet.getOutputDirectory()).response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -80,8 +81,8 @@ public void responseWithHeaders() throws IOException { public void responseWithContent() throws IOException { this.snippet.expectHttpResponse("response-with-content").withContents( httpResponse(OK).content("content")); - new HttpResponseSnippet().document(new OperationBuilder("response-with-content") - .response().content("content").build()); + new HttpResponseSnippet().document(new OperationBuilder("response-with-content", + this.snippet.getOutputDirectory()).response().content("content").build()); } @Test @@ -94,9 +95,10 @@ public void responseWithCustomSnippetAttributes() throws IOException { new FileSystemResource( "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) - .document(new OperationBuilder("response-with-snippet-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .document(new OperationBuilder("response-with-snippet-attributes", + this.snippet.getOutputDirectory()).attribute( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java similarity index 90% rename from spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 97979bdc9..f5a514809 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -32,7 +32,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.FileSystemResource; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -45,7 +44,7 @@ /** * Tests for {@link LinksSnippet} - * + * * @author Andy Wilkinson */ public class LinksSnippetTests { @@ -63,7 +62,7 @@ public void undocumentedLink() throws IOException { + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), Collections. emptyList()).document(new OperationBuilder( - "undocumented-link").build()); + "undocumented-link", this.snippet.getOutputDirectory()).build()); } @Test @@ -72,8 +71,8 @@ public void missingLink() throws IOException { this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar"))).document(new OperationBuilder("missing-link") - .build()); + .description("bar"))).document(new OperationBuilder("missing-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -83,7 +82,8 @@ public void documentedOptionalLink() throws IOException { .row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link").build()); + .document(new OperationBuilder("documented-optional-link", this.snippet + .getOutputDirectory()).build()); } @Test @@ -93,7 +93,7 @@ public void missingOptionalLink() throws IOException { .row("foo", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") .description("bar").optional())).document(new OperationBuilder( - "missing-optional-link").build()); + "missing-optional-link", this.snippet.getOutputDirectory()).build()); } @Test @@ -104,8 +104,8 @@ public void undocumentedLinkAndMissingLink() throws IOException { + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("undocumented-link-and-missing-link") - .build()); + .document(new OperationBuilder("undocumented-link-and-missing-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -118,7 +118,8 @@ public void documentedLinks() throws IOException { new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))) - .document(new OperationBuilder("documented-links").build()); + .document(new OperationBuilder("documented-links", this.snippet + .getOutputDirectory()).build()); } @Test @@ -127,23 +128,20 @@ public void linksWithCustomDescriptorAttributes() throws IOException { tableWithHeader("Relation", "Description", "Foo") // .row("a", "one", "alpha") // .row("b", "two", "bravo")); - MockHttpServletRequest request = new MockHttpServletRequest(); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("links")) .thenReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); - request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( - resolver)); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one").attributes( key("foo").value("alpha")), new LinkDescriptor("b").description("two").attributes( key("foo").value("bravo")))).document(new OperationBuilder( - "links-with-custom-descriptor-attributes").attribute( - TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); + "links-with-custom-descriptor-attributes", this.snippet + .getOutputDirectory()).attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } @Test @@ -160,7 +158,8 @@ public void linksWithCustomAttributes() throws IOException { "Title for the links")), Arrays.asList( new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))) - .document(new OperationBuilder("links-with-custom-attributes").attribute( + .document(new OperationBuilder("links-with-custom-attributes", + this.snippet.getOutputDirectory()).attribute( TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java similarity index 82% rename from spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 0a31fd8f9..f13509b5f 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -1,3 +1,19 @@ +/* + * 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.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; @@ -20,7 +36,7 @@ /** * Tests for {@link ContentModifyingOperationPreprocessor} - * + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java new file mode 100644 index 000000000..fdfe17c73 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -0,0 +1,60 @@ +/* + * 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.operation.preprocess; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.restdocs.operation.OperationRequest; + +/** + * Tests for {@link DelegatingOperationRequestPreprocessor} + * + * @author Andy Wilkinson + */ +public class DelegatingOperationRequestPreprocessorTests { + + @Test + public void delegationOccurs() { + OperationRequest originalRequest = mock(OperationRequest.class); + + OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); + OperationRequest preprocessedRequest1 = mock(OperationRequest.class); + when(preprocessor1.preprocess(originalRequest)).thenReturn(preprocessedRequest1); + + OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class); + OperationRequest preprocessedRequest2 = mock(OperationRequest.class); + when(preprocessor2.preprocess(preprocessedRequest1)).thenReturn( + preprocessedRequest2); + + OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); + OperationRequest preprocessedRequest3 = mock(OperationRequest.class); + when(preprocessor3.preprocess(preprocessedRequest2)).thenReturn( + preprocessedRequest3); + + OperationRequest result = new DelegatingOperationRequestPreprocessor( + Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) + .preprocess(originalRequest); + + assertThat(result, is(preprocessedRequest3)); + } +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java new file mode 100644 index 000000000..764eb63d4 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -0,0 +1,61 @@ +/* + * 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.operation.preprocess; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * Tests for {@link DelegatingOperationResponsePreprocessor} + * + * @author Andy Wilkinson + */ +public class DelegatingOperationResponsePreprocessorTests { + + @Test + public void delegationOccurs() { + OperationResponse originalResponse = mock(OperationResponse.class); + + OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); + OperationResponse preprocessedResponse1 = mock(OperationResponse.class); + when(preprocessor1.preprocess(originalResponse)) + .thenReturn(preprocessedResponse1); + + OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class); + OperationResponse preprocessedResponse2 = mock(OperationResponse.class); + when(preprocessor2.preprocess(preprocessedResponse1)).thenReturn( + preprocessedResponse2); + + OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); + OperationResponse preprocessedResponse3 = mock(OperationResponse.class); + when(preprocessor3.preprocess(preprocessedResponse2)).thenReturn( + preprocessedResponse3); + + OperationResponse result = new DelegatingOperationResponsePreprocessor( + Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) + .preprocess(originalResponse); + + assertThat(result, is(preprocessedResponse3)); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java similarity index 78% rename from spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index c90f4afdc..cf710c818 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -1,3 +1,19 @@ +/* + * 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.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; @@ -22,7 +38,7 @@ /** * Tests for {@link HeaderRemovingOperationPreprocessorTests} - * + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java similarity index 86% rename from spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index 2763d6465..aa881ba80 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -1,3 +1,19 @@ +/* + * 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.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; @@ -20,7 +36,7 @@ /** * Tests for {@link LinkMaskingContentModifier} - * + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java similarity index 58% rename from spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index f419296d6..c7a1e2c93 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -1,3 +1,19 @@ +/* + * 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.operation.preprocess; import static org.hamcrest.CoreMatchers.equalTo; @@ -10,7 +26,7 @@ /** * Tests for {@link PatternReplacingContentModifier} - * + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java similarity index 87% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index bec7ee7eb..35168549b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -67,7 +67,8 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))).document(new OperationBuilder( - "map-request-with-fields").request("http://localhost") + "map-request-with-fields", this.snippet.getOutputDirectory()) + .request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -82,7 +83,8 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a") .description("three"))).document(new OperationBuilder( - "array-request-with-fields").request("http://localhost") + "array-request-with-fields", this.snippet.getOutputDirectory()) + .request("http://localhost") .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); } @@ -93,8 +95,9 @@ public void undocumentedRequestField() throws IOException { .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections. emptyList()) - .document(new OperationBuilder("undocumented-request-field") - .request("http://localhost").content("{\"a\": 5}").build()); + .document(new OperationBuilder("undocumented-request-field", this.snippet + .getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5}").build()); } @Test @@ -104,8 +107,9 @@ public void missingRequestField() throws IOException { .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields") - .request("http://localhost").content("{}").build()); + .document(new OperationBuilder("missing-request-fields", this.snippet + .getOutputDirectory()).request("http://localhost").content("{}") + .build()); } @Test @@ -113,8 +117,9 @@ public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type") - .request("http://localhost").content("{ }").build()); + "missing-optional-request-field-with-no-type", this.snippet + .getOutputDirectory()).request("http://localhost").content("{ }") + .build()); } @Test @@ -128,9 +133,9 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) .document(new OperationBuilder( - "undocumented-request-field-and-missing-request-field") - .request("http://localhost").content("{ \"a\": { \"c\": 5 }}") - .build()); + "undocumented-request-field-and-missing-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{ \"a\": { \"c\": 5 }}").build()); } @Test @@ -151,7 +156,8 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( key("foo").value("charlie")))).document(new OperationBuilder( - "request-fields-with-custom-descriptor-attributes") + "request-fields-with-custom-descriptor-attributes", this.snippet + .getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); @@ -166,7 +172,8 @@ public void requestFieldsWithCustomAttributes() throws IOException { snippetResource("request-fields-with-title")); new RequestFieldsSnippet(attributes(key("title").value("Custom title")), Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("request-fields-with-custom-attributes") + .document(new OperationBuilder("request-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost").content("{\"a\": \"foo\"}").build()); @@ -183,7 +190,8 @@ public void xmlRequestFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-request") + .document(new OperationBuilder("xml-request", this.snippet + .getOutputDirectory()) .request("http://localhost") .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -197,7 +205,8 @@ public void undocumentedXmlRequestField() throws IOException { .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections. emptyList()) - .document(new OperationBuilder("undocumented-xml-request-field") + .document(new OperationBuilder("undocumented-xml-request-field", + this.snippet.getOutputDirectory()) .request("http://localhost") .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -208,7 +217,8 @@ public void undocumentedXmlRequestField() throws IOException { public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request") + .document(new OperationBuilder("missing-xml-request", this.snippet + .getOutputDirectory()) .request("http://localhost") .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -223,8 +233,8 @@ public void missingXmlRequestField() throws IOException { + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-request-fields").request("http://localhost") - .content("") + "missing-xml-request-fields", this.snippet.getOutputDirectory()) + .request("http://localhost").content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } @@ -240,7 +250,8 @@ public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOExce + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field") + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) .request("http://localhost") .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java similarity index 87% rename from spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 9018f96d7..16f5a8533 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -35,7 +35,6 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -44,7 +43,7 @@ import org.springframework.restdocs.test.OperationBuilder; /** - * Tests for {@link PayloadDocumentation} + * Tests for {@link ReponseFieldsSnippet} * * @author Andy Wilkinson */ @@ -72,7 +71,8 @@ public void mapResponseWithFields() throws IOException { fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(new OperationBuilder("map-response-with-fields") + .document(new OperationBuilder("map-response-with-fields", this.snippet + .getOutputDirectory()) .response() .content( "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" @@ -90,7 +90,8 @@ public void arrayResponseWithFields() throws IOException { new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") .description("two"), fieldWithPath("[]a").description("three"))) - .document(new OperationBuilder("array-response-with-fields").response() + .document(new OperationBuilder("array-response-with-fields", this.snippet + .getOutputDirectory()).response() .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); } @@ -101,7 +102,8 @@ public void arrayResponse() throws IOException { tableWithHeader("Path", "Type", "Description") // .row("[]", "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(new OperationBuilder("array-response").response() + .document(new OperationBuilder("array-response", this.snippet + .getOutputDirectory()).response() .content("[\"a\", \"b\", \"c\"]").build()); } @@ -123,7 +125,8 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( key("foo").value("charlie")))).document(new OperationBuilder( - "response-fields-with-custom-attributes") + "response-fields-with-custom-attributes", this.snippet + .getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); @@ -138,7 +141,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { snippetResource("response-fields-with-title")); new ResponseFieldsSnippet(attributes(key("title").value("Custom title")), Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("response-fields-with-custom-attributes") + .document(new OperationBuilder("response-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .content("{\"a\": \"foo\"}").build()); @@ -154,7 +158,8 @@ public void xmlResponseFields() throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-response") + .document(new OperationBuilder("xml-response", this.snippet + .getOutputDirectory()) .response() .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -167,11 +172,9 @@ public void undocumentedXmlResponseField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_XML_VALUE); - response.getOutputStream().print("5"); new ResponseFieldsSnippet(Collections. emptyList()) - .document(new OperationBuilder("undocumented-xml-response-field") + .document(new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()) .response() .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -181,11 +184,9 @@ public void undocumentedXmlResponseField() throws IOException { @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_XML_VALUE); - response.getOutputStream().print("5"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type") + .document(new OperationBuilder("xml-response-no-field-type", this.snippet + .getOutputDirectory()) .response() .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -198,12 +199,10 @@ public void missingXmlResponseField() throws IOException { this.thrown .expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a/b]")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_XML_VALUE); - response.getOutputStream().print(""); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-response-field").response().content("") + "missing-xml-response-field", this.snippet.getOutputDirectory()) + .response().content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } @@ -218,12 +217,10 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_XML_VALUE); - response.getOutputStream().print("5"); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field") + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) .response() .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java similarity index 87% rename from spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c4204d8c4..5c5657c87 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -61,7 +61,8 @@ public void undocumentedPathParameter() throws IOException { this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not documented: [a]")); new PathParametersSnippet(Collections. emptyList()) - .document(new OperationBuilder("undocumented-path-parameter").attribute( + .document(new OperationBuilder("undocumented-path-parameter", + this.snippet.getOutputDirectory()).attribute( "org.springframework.restdocs.urlTemplate", "/{a}/").build()); } @@ -72,7 +73,8 @@ public void missingPathParameter() throws IOException { + " not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter").attribute( + .document(new OperationBuilder("missing-path-parameter", this.snippet + .getOutputDirectory()).attribute( "org.springframework.restdocs.urlTemplate", "/").build()); } @@ -84,9 +86,10 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " names were not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("undocumented-and-missing-path-parameters") - .attribute("org.springframework.restdocs.urlTemplate", "/{b}") - .build()); + .document(new OperationBuilder( + "undocumented-and-missing-path-parameters", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{b}").build()); } @Test @@ -97,8 +100,8 @@ public void pathParameters() throws IOException { new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") .description("two"))).document(new OperationBuilder( - "path-parameters").attribute("org.springframework.restdocs.urlTemplate", - "/{a}/{b}").build()); + "path-parameters", this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); } @Test @@ -109,9 +112,11 @@ public void pathParametersWithQueryString() throws IOException { .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "path-parameters-with-query-string").attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); + .description("two"))) + .document(new OperationBuilder("path-parameters-with-query-string", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar") + .build()); } @Test @@ -127,7 +132,8 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) .document(new OperationBuilder( - "path-parameters-with-custom-descriptor-attributes") + "path-parameters-with-custom-descriptor-attributes", this.snippet + .getOutputDirectory()) .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); @@ -146,7 +152,8 @@ public void pathParametersWithCustomAttributes() throws IOException { parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document(new OperationBuilder("path-parameters-with-custom-attributes") + .document(new OperationBuilder("path-parameters-with-custom-attributes", + this.snippet.getOutputDirectory()) .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java similarity index 87% rename from spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 0ec85773d..5a071ffd0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -60,8 +60,9 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections. emptyList()) - .document(new OperationBuilder("undocumented-parameter") - .request("http://localhost").param("a", "alpha").build()); + .document(new OperationBuilder("undocumented-parameter", this.snippet + .getOutputDirectory()).request("http://localhost") + .param("a", "alpha").build()); } @Test @@ -71,8 +72,8 @@ public void missingParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder("missing-parameter").request( - "http://localhost").build()); + "one"))).document(new OperationBuilder("missing-parameter", this.snippet + .getOutputDirectory()).request("http://localhost").build()); } @Test @@ -84,8 +85,8 @@ public void undocumentedAndMissingParameters() throws IOException { + " names were not found in the request: [a]")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( "one"))).document(new OperationBuilder( - "undocumented-and-missing-parameters").request("http://localhost") - .param("b", "bravo").build()); + "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost").param("b", "bravo").build()); } @Test @@ -96,8 +97,9 @@ public void requestParameters() throws IOException { new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") .description("two"))).document(new OperationBuilder( - "request-parameters").request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + "request-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost").param("a", "bravo").param("b", "bravo") + .build()); } @Test @@ -114,7 +116,8 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException key("foo").value("alpha")), parameterWithName("b").description("two").attributes( key("foo").value("bravo")))).document(new OperationBuilder( - "request-parameters-with-custom-descriptor-attributes") + "request-parameters-with-custom-descriptor-attributes", this.snippet + .getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .param("a", "alpha").param("b", "bravo").build()); @@ -134,7 +137,8 @@ public void requestParametersWithCustomAttributes() throws IOException { key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) .document(new OperationBuilder( - "request-parameters-with-custom-attributes") + "request-parameters-with-custom-attributes", this.snippet + .getOutputDirectory()) .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost").param("a", "alpha") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java new file mode 100644 index 000000000..ff06d8b3a --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -0,0 +1,65 @@ +/* + * 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.snippet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * Tests for {@link RestDocumentationContextPlaceholderResolver} + * + * @author Andy Wilkinson + * + */ +public class RestDocumentationContextPlaceholderResolverTests { + + @Test + public void dashSeparatedMethodName() throws Exception { + assertThat( + createResolver("dashSeparatedMethodName").resolvePlaceholder( + "method-name"), equalTo("dash-separated-method-name")); + } + + @Test + public void underscoreSeparatedMethodName() throws Exception { + assertThat( + createResolver("underscoreSeparatedMethodName").resolvePlaceholder( + "method_name"), equalTo("underscore_separated_method_name")); + } + + @Test + public void camelCaseMethodName() throws Exception { + assertThat( + createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), + equalTo("camelCaseMethodName")); + } + + @Test + public void stepCount() throws Exception { + assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("0")); + } + + private PlaceholderResolver createResolver(String methodName) { + return new RestDocumentationContextPlaceholderResolver( + new RestDocumentationContext(null, methodName, null)); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java similarity index 65% rename from spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index c1882d27e..2f186dd0d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -24,7 +24,7 @@ import java.io.File; import org.junit.Test; -import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; /** @@ -41,41 +41,33 @@ public class StandardWriterResolverTests { @Test public void noConfiguredOutputDirectoryAndRelativeInput() { - assertThat(this.resolver.resolveFile("foo", "bar.txt"), is(nullValue())); + assertThat(this.resolver.resolveFile("foo", "bar.txt", + new RestDocumentationContext(null, null, null)), is(nullValue())); } @Test public void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); - assertThat(this.resolver.resolveFile(absolutePath, "bar.txt"), is(new File( + assertThat(this.resolver.resolveFile(absolutePath, "bar.txt", + new RestDocumentationContext(null, null, null)), is(new File( absolutePath, "bar.txt"))); } @Test public void configuredOutputAndRelativeInput() { - String outputDir = new File("foo").getAbsolutePath(); - System.setProperty("org.springframework.restdocs.outputDir", outputDir); - try { - assertThat(this.resolver.resolveFile("bar", "baz.txt"), is(new File( - outputDir, "bar/baz.txt"))); - } - finally { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + File outputDir = new File("foo").getAbsoluteFile(); + assertThat(this.resolver.resolveFile("bar", "baz.txt", + new RestDocumentationContext(null, null, outputDir)), is(new File( + outputDir, "bar/baz.txt"))); } @Test public void configuredOutputAndAbsoluteInput() { - String outputDir = new File("foo").getAbsolutePath(); + File outputDir = new File("foo").getAbsoluteFile(); String absolutePath = new File("bar").getAbsolutePath(); - System.setProperty("org.springframework.restdocs.outputDir", outputDir); - try { - assertThat(this.resolver.resolveFile(absolutePath, "baz.txt"), is(new File( - absolutePath, "baz.txt"))); - } - finally { - System.clearProperty("org.springframework.restdocs.outputDir"); - } + assertThat(this.resolver.resolveFile(absolutePath, "baz.txt", + new RestDocumentationContext(null, null, outputDir)), is(new File( + absolutePath, "baz.txt"))); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java similarity index 77% rename from spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index c3b72eb3b..9786aa206 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -43,37 +43,13 @@ public class ExpectedSnippet implements TestRule { private SnippetMatcher snippet = SnippetMatchers.snippet(); - private File outputDir; + private File outputDirectory; @Override public Statement apply(final Statement base, Description description) { - this.outputDir = new File("build/" + description.getTestClass().getSimpleName()); - return new OutputDirectoryStatement(new ExpectedSnippetStatement(base), - this.outputDir); - } - - private static final class OutputDirectoryStatement extends Statement { - - private final Statement delegate; - - private final File outputDir; - - public OutputDirectoryStatement(Statement delegate, File outputDir) { - this.delegate = delegate; - this.outputDir = outputDir; - } - - @Override - public void evaluate() throws Throwable { - System.setProperty("org.springframework.restdocs.outputDir", - this.outputDir.getAbsolutePath()); - try { - this.delegate.evaluate(); - } - finally { - System.clearProperty("org.springframework.restdocs.outputDir"); - } - } + this.outputDirectory = new File("build/" + + description.getTestClass().getSimpleName()); + return new ExpectedSnippetStatement(base); } private final class ExpectedSnippetStatement extends Statement { @@ -93,8 +69,8 @@ public void evaluate() throws Throwable { } private void verifySnippet() throws IOException { - if (this.outputDir != null && this.expectedName != null) { - File snippetDir = new File(this.outputDir, this.expectedName); + if (this.outputDirectory != null && this.expectedName != null) { + File snippetDir = new File(this.outputDirectory, this.expectedName); File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); assertThat(snippetFile, is(this.snippet)); } @@ -150,4 +126,8 @@ public void withContents(Matcher matcher) { this.snippet.withContents(matcher); } + public File getOutputDirectory() { + return this.outputDirectory; + } + } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java similarity index 86% rename from spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index fbb193377..e28d5751d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,22 @@ +/* + * 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.test; +import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -9,8 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.config.RestDocumentationContext; -import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; @@ -20,6 +36,7 @@ import org.springframework.restdocs.operation.StandardOperationRequest; import org.springframework.restdocs.operation.StandardOperationRequestPart; import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -34,10 +51,13 @@ public class OperationBuilder { private final String name; + private final File outputDirectory; + private OperationRequestBuilder requestBuilder; - public OperationBuilder(String name) { + public OperationBuilder(String name, File outputDirectory) { this.name = name; + this.outputDirectory = outputDirectory; } public OperationRequestBuilder request(String uri) { @@ -59,7 +79,8 @@ public Operation build() { this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine(new StandardTemplateResourceResolver())); } - RestDocumentationContext context = new RestDocumentationContext(null); + RestDocumentationContext context = new RestDocumentationContext(null, null, + this.outputDirectory); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context))); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java similarity index 100% rename from spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json b/spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json rename to spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-embedded-and-links.json diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json b/spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-embedded.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-embedded.json rename to spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-embedded.json diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json b/spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-links.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/multiple-fields-and-links.json rename to spring-restdocs-core/src/test/resources/field-payloads/multiple-fields-and-links.json diff --git a/spring-restdocs/src/test/resources/field-payloads/multiple-fields.json b/spring-restdocs-core/src/test/resources/field-payloads/multiple-fields.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/multiple-fields.json rename to spring-restdocs-core/src/test/resources/field-payloads/multiple-fields.json diff --git a/spring-restdocs/src/test/resources/field-payloads/no-fields.json b/spring-restdocs-core/src/test/resources/field-payloads/no-fields.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/no-fields.json rename to spring-restdocs-core/src/test/resources/field-payloads/no-fields.json diff --git a/spring-restdocs/src/test/resources/field-payloads/single-field.json b/spring-restdocs-core/src/test/resources/field-payloads/single-field.json similarity index 100% rename from spring-restdocs/src/test/resources/field-payloads/single-field.json rename to spring-restdocs-core/src/test/resources/field-payloads/single-field.json diff --git a/spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-different-rels.json rename to spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json diff --git a/spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/atom/multiple-links-same-rels.json rename to spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json diff --git a/spring-restdocs/src/test/resources/link-payloads/atom/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/atom/no-links.json rename to spring-restdocs-core/src/test/resources/link-payloads/atom/no-links.json diff --git a/spring-restdocs/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/atom/single-link.json rename to spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json diff --git a/spring-restdocs/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/atom/wrong-format.json rename to spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json diff --git a/spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-different-rels.json rename to spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json diff --git a/spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/hal/multiple-links-same-rels.json rename to spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json diff --git a/spring-restdocs/src/test/resources/link-payloads/hal/no-links.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/hal/no-links.json rename to spring-restdocs-core/src/test/resources/link-payloads/hal/no-links.json diff --git a/spring-restdocs/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/hal/single-link.json rename to spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json diff --git a/spring-restdocs/src/test/resources/link-payloads/hal/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json similarity index 100% rename from spring-restdocs/src/test/resources/link-payloads/hal/wrong-format.json rename to spring-restdocs-core/src/test/resources/link-payloads/hal/wrong-format.json diff --git a/spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties similarity index 100% rename from spring-restdocs/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties rename to spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle new file mode 100644 index 000000000..597741053 --- /dev/null +++ b/spring-restdocs-mockmvc/build.gradle @@ -0,0 +1,14 @@ +dependencies { + compile 'org.springframework:spring-test' + compile project(':spring-restdocs-core') + + testCompile 'org.mockito:mockito-core' + testCompile 'org.hamcrest:hamcrest-library' + testCompile 'org.springframework.hateoas:spring-hateoas' + testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') + testRuntime 'commons-logging:commons-logging:1.2' +} + +test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java similarity index 95% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java index f84530585..ebb0a4c48 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.mockmvc; import org.springframework.mock.web.MockHttpServletRequest; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java similarity index 97% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java index ce542926e..57153d70a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.mockmvc; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java similarity index 91% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 60c82404f..70d4351ee 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.restdocs.operation; +package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.util.IterableEnumeration.iterable; +import static org.springframework.restdocs.mockmvc.util.IterableEnumeration.iterable; import java.io.IOException; import java.io.PrintWriter; @@ -34,6 +34,11 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.StandardOperationRequest; +import org.springframework.restdocs.operation.StandardOperationRequestPart; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -45,7 +50,7 @@ * @author Andy Wilkinson * */ -public class MockMvcOperationRequestFactory { +class MockMvcOperationRequestFactory { private static final String SCHEME_HTTP = "http"; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java similarity index 88% rename from spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java index 5ace0f069..0f6f6b8ab 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java @@ -14,11 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs.operation; +package org.springframework.restdocs.mockmvc; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperationResponse; /** * A factory for creating an {@link OperationResponse} derived from a @@ -26,7 +28,7 @@ * * @author Andy Wilkinson */ -public class MockMvcOperationResponseFactory { +class MockMvcOperationResponseFactory { /** * Create a new {@code OperationResponse} derived from the given {@code mockResponse}. diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java similarity index 90% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 952ca4230..2a2cc2d6e 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.mockmvc; -import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -30,21 +30,23 @@ * * @author Andy Wilkinson */ -public abstract class RestDocumentation { +public abstract class MockMvcRestDocumentation { - private RestDocumentation() { + private MockMvcRestDocumentation() { } /** - * Provides access to a {@link MockMvcConfigurer} that can be used to configure the - * REST documentation when building a {@link MockMvc} instance. + * Provides access to a {@link MockMvcConfigurer} that can be used to configure a + * {@link MockMvc} instance using the given {@code restDocumentation}. * - * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * @param restDocumentation the REST documentation * @return the configurer + * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) */ - public static RestDocumentationConfigurer documentationConfiguration() { - return new RestDocumentationConfigurer(); + public static RestDocumentationMockMvcConfigurer documentationConfiguration( + RestDocumentation restDocumentation) { + return new RestDocumentationMockMvcConfigurer(restDocumentation); } /** diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java similarity index 86% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java index 25c577673..2fc73bb37 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.mockmvc; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; /** * A configurer that is nested and, therefore, has a parent. * - * @author awilkinson + * @author Andy Wilkinson * @param The parent's type */ -public interface NestedConfigurer { +interface NestedConfigurer { /** * Returns the configurer's parent diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java similarity index 79% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 2f9417b90..e13025eb3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package org.springframework.restdocs.config; - -import java.util.Arrays; -import java.util.List; +package org.springframework.restdocs.mockmvc; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -39,9 +38,9 @@ * @author Andy Wilkinson * @author Dmitriy Mayboroda * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) - * @see RestDocumentation#documentationConfiguration() + * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) */ -public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { +public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter { private final UriConfigurer uriConfigurer = new UriConfigurer(this); @@ -54,15 +53,17 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); /** - * Creates a new {@link RestDocumentationConfigurer}. - * @see RestDocumentation#documentationConfiguration() + * Creates a new {code RestDocumentationMockMvcConfigurer} that will use the given + * {@code restDocumentation} when configuring MockMvc. + * + * @param restDocumentation the rest documentation + * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) */ - public RestDocumentationConfigurer() { + RestDocumentationMockMvcConfigurer(RestDocumentation restDocumentation) { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( - Arrays. asList(this.uriConfigurer, - this.writerResolverConfigurer, this.snippetConfigurer, - new StepCountConfigurer(), new ContentLengthHeaderConfigurer(), - this.templateEngineConfigurer)); + restDocumentation, this.uriConfigurer, this.writerResolverConfigurer, + this.snippetConfigurer, new ContentLengthHeaderConfigurer(), + this.templateEngineConfigurer); } /** @@ -91,7 +92,7 @@ public SnippetConfigurer snippets() { * @param templateEngine the template engine to use * @return {@code this} */ - public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) { + public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine templateEngine) { this.templateEngineConfigurer.setTemplateEngine(templateEngine); return this; } @@ -103,7 +104,7 @@ public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) * @param writerResolver The writer resolver to use * @return {@code this} */ - public RestDocumentationConfigurer writerResolver(WriterResolver writerResolver) { + public RestDocumentationMockMvcConfigurer writerResolver(WriterResolver writerResolver) { this.writerResolverConfigurer.setWriterResolver(writerResolver); return this; } @@ -114,19 +115,6 @@ public RequestPostProcessor beforeMockMvcCreated( return this.requestPostProcessor; } - private static class StepCountConfigurer extends AbstractConfigurer { - - @Override - void apply(MockHttpServletRequest request) { - RestDocumentationContext context = (RestDocumentationContext) request - .getAttribute(RestDocumentationContext.class.getName()); - if (context != null) { - context.getAndIncrementStepCount(); - } - } - - } - private static class ContentLengthHeaderConfigurer extends AbstractConfigurer { @Override @@ -182,17 +170,20 @@ void setWriterResolver(WriterResolver writerResolver) { private static class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { - private final List configurers; + private final RestDocumentation restDocumentation; + + private final AbstractConfigurer[] configurers; private ConfigurerApplyingRequestPostProcessor( - List configurers) { + RestDocumentation restDocumentation, AbstractConfigurer... configurers) { + this.restDocumentation = restDocumentation; this.configurers = configurers; } @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { request.setAttribute(RestDocumentationContext.class.getName(), - RestDocumentationContextHolder.getCurrentContext()); + this.restDocumentation.beforeOperation()); for (AbstractConfigurer configurer : this.configurers) { configurer.apply(request); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java similarity index 99% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 160006d40..dc6f7a25c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.mockmvc; import java.net.URI; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java similarity index 93% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index e1fc99f11..2a7e4ab4c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.util.IterableEnumeration.iterable; +import static org.springframework.restdocs.mockmvc.util.IterableEnumeration.iterable; import java.util.ArrayList; import java.util.Arrays; @@ -24,8 +24,6 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.operation.MockMvcOperationRequestFactory; -import org.springframework.restdocs.operation.MockMvcOperationResponseFactory; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -42,7 +40,7 @@ * * @author Andy Wilkinson * @author Andreas Evers - * @see RestDocumentation#document(String, Snippet...) + * @see MockMvcRestDocumentation#document(String, Snippet...) */ public class RestDocumentationResultHandler implements ResultHandler { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java similarity index 93% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index 992086f73..ace722ea1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.mockmvc; import java.util.Arrays; import java.util.List; @@ -29,10 +29,9 @@ * A configurer that can be used to configure the generated documentation snippets. * * @author Andy Wilkinson - * */ public class SnippetConfigurer extends - AbstractNestedConfigurer { + AbstractNestedConfigurer { private List defaultSnippets = Arrays.asList( CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), @@ -46,7 +45,7 @@ public class SnippetConfigurer extends private String snippetEncoding = DEFAULT_SNIPPET_ENCODING; - SnippetConfigurer(RestDocumentationConfigurer parent) { + SnippetConfigurer(RestDocumentationMockMvcConfigurer parent) { super(parent); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java similarity index 93% rename from spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index 30baa03c2..c931ea967 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.config; +package org.springframework.restdocs.mockmvc; import org.springframework.mock.web.MockHttpServletRequest; @@ -23,7 +23,7 @@ * * @author Andy Wilkinson */ -public class UriConfigurer extends AbstractNestedConfigurer { +public class UriConfigurer extends AbstractNestedConfigurer { /** * The default scheme for documented URIs @@ -49,7 +49,7 @@ public class UriConfigurer extends AbstractNestedConfigurer - repackJar.baseName = "restdocs-jmustache-repack" - repackJar.version = dependencyManagement.managedVersions['com.samskivert:jmustache'] - - doLast() { - project.ant { - taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", - classpath: configurations.jarjar.asPath - jarjar(destfile: repackJar.archivePath) { - configurations.jmustache.each { originalJar -> - zipfileset(src: originalJar, includes: '**/*.class') - } - rule(pattern: 'com.samskivert.**', result: 'org.springframework.restdocs.@1') - } - } - } -} - -dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'junit:junit' - compile 'org.springframework:spring-core' - compile 'org.springframework:spring-test' - compile 'org.springframework:spring-web' - compile 'org.springframework:spring-webmvc' - compile 'javax.servlet:javax.servlet-api' - compile files(jmustacheRepackJar) - jacoco 'org.jacoco:org.jacoco.agent::runtime' - jarjar 'com.googlecode.jarjar:jarjar:1.3' - jmustache 'com.samskivert:jmustache@jar' - optional 'javax.validation:validation-api' - testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.mockito:mockito-core' - testCompile 'org.hamcrest:hamcrest-core' - testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.hibernate:hibernate-validator' - testRuntime 'org.glassfish:javax.el:3.0.0' -} - -jar { - dependsOn jmustacheRepackJar - from(zipTree(jmustacheRepackJar.archivePath)) { - include "org/springframework/restdocs/**" - } -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from project.sourceSets.main.allSource -} - -javadoc { - description = "Generates project-level javadoc for use in -javadoc jar" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = "Spring REST Docs $version" - options.docTitle = "${options.header} API" - options.links = [ - 'http://docs.oracle.com/javase/8/docs/api/', - "http://docs.spring.io/spring-framework/docs/${dependencyManagement.managedVersions['org.springframework:spring-core']}/javadoc-api/", - 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/' - ] as String[] - options.addStringOption '-quiet' -} - -task javadocJar(type: Jar) { - classifier = "javadoc" - from javadoc -} - -artifacts { - archives sourcesJar - archives javadocJar -} - -test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*,excludes=org.springframework.restdocs.mustache.*" - testLogging { - exceptionFormat "full" - } -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java deleted file mode 100644 index 2a95a7f18..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java +++ /dev/null @@ -1,73 +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.config; - -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.test.context.TestContext; - -/** - * {@code RestDocumentationContext} encapsulates the context in which the documentation of - * a RESTful API is being performed. - * - * @author Andy Wilkinson - */ -public final class RestDocumentationContext { - - private final AtomicInteger stepCount = new AtomicInteger(0); - - private final TestContext testContext; - - /** - * Creates a new {@code RestDocumentationContext} backed by the given - * {@code testContext}. - * - * @param testContext the test context - */ - public RestDocumentationContext(TestContext testContext) { - this.testContext = testContext; - } - - /** - * Returns the test {@link Method method} that is currently executing - * - * @return The test method - */ - public Method getTestMethod() { - return this.testContext == null ? null : this.testContext.getTestMethod(); - } - - /** - * Gets and then increments the current step count - * - * @return The step count prior to it being incremented - */ - int getAndIncrementStepCount() { - return this.stepCount.getAndIncrement(); - } - - /** - * Gets the current step count - * - * @return The current step count - */ - public int getStepCount() { - return this.stepCount.get(); - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java deleted file mode 100644 index 7189ad0a1..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java +++ /dev/null @@ -1,41 +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.config; - -import org.springframework.test.context.TestContext; -import org.springframework.test.context.TestExecutionListener; -import org.springframework.test.context.support.AbstractTestExecutionListener; - -/** - * A {@link TestExecutionListener} that sets up and tears down the Spring REST Docs - * context for each test method - * - * @author Andy Wilkinson - */ -public class RestDocumentationTestExecutionListener extends AbstractTestExecutionListener { - - @Override - public void beforeTestMethod(TestContext testContext) throws Exception { - RestDocumentationContextHolder.setCurrentContext(new RestDocumentationContext( - testContext)); - } - - @Override - public void afterTestMethod(TestContext testContext) throws Exception { - RestDocumentationContextHolder.removeCurrentContext(); - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java deleted file mode 100644 index a8ea46fc7..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.springframework.restdocs.operation.preprocess; - -import org.springframework.restdocs.operation.OperationResponse; - -/** - * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} - * prior to it being documented. - * - * @author Andy Wilkinson - */ -public interface OperationResponsePreprocessor { - - /** - * Processes and potentially modifies the given {@code response} before it is - * documented. - * - * @param response the response - * @return the modified response - */ - OperationResponse preprocess(OperationResponse response); - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java deleted file mode 100644 index a569b39b9..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.springframework.restdocs.payload; - -/** - * Thrown to indicate that a failure has occurred during payload handling - * - * @author Andy Wilkinson - * - */ -@SuppressWarnings("serial") -class PayloadHandlingException extends RuntimeException { - - /** - * Creates a new {@code PayloadHandlingException} with the given cause - * @param cause the cause of the failure - */ - PayloadHandlingException(Throwable cause) { - super(cause); - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java deleted file mode 100644 index 3241ce04f..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationProperties.java +++ /dev/null @@ -1,52 +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.snippet; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import org.springframework.util.StringUtils; - -class DocumentationProperties { - - private final Properties properties = new Properties(); - - DocumentationProperties() { - try (InputStream stream = getClass().getClassLoader().getResourceAsStream( - "documentation.properties")) { - if (stream != null) { - this.properties.load(stream); - } - } - catch (IOException ex) { - throw new IllegalStateException("Failed to read documentation.properties", ex); - } - - this.properties.putAll(System.getProperties()); - } - - File getOutputDir() { - String outputDir = this.properties - .getProperty("org.springframework.restdocs.outputDir"); - if (StringUtils.hasText(outputDir)) { - return new File(outputDir).getAbsoluteFile(); - } - return null; - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java deleted file mode 100644 index 302e5f6ca..000000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.springframework.restdocs.templates; - -import java.util.Map; - -/** - * A compiled {@code Template} that can be rendered to a {@link String}. - * - * @author Andy Wilkinson - * - */ -public interface Template { - - /** - * Renders the template to a {@link String} using the given {@code context} for - * variable/property resolution. - * - * @param context The context to use - * @return The rendered template - */ - String render(Map context); - -} diff --git a/spring-restdocs/src/main/resources/META-INF/spring.factories b/spring-restdocs/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 64519d788..000000000 --- a/spring-restdocs/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.test.context.TestExecutionListener=org.springframework.restdocs.config.RestDocumentationTestExecutionListener \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java deleted file mode 100644 index 629d6908b..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationContexts.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.springframework.restdocs.config; - - -public class RestDocumentationContexts { - -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java deleted file mode 100644 index 03b216416..000000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.springframework.restdocs.snippet; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Method; - -import org.junit.Test; -import org.springframework.restdocs.config.RestDocumentationContext; -import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver; -import org.springframework.test.context.TestContext; -import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; - -/** - * Tests for {@link RestDocumentationContextPlaceholderResolver} - * - * @author Andy Wilkinson - * - */ -public class RestDocumentationContextPlaceholderResolverTests { - - private final TestContext testContext = mock(TestContext.class); - - private final RestDocumentationContext context = new RestDocumentationContext( - this.testContext); - - private final PlaceholderResolver resolver = new RestDocumentationContextPlaceholderResolver( - this.context); - - @Test - public void dashSeparatedMethodName() throws Exception { - when(this.testContext.getTestMethod()).thenReturn( - getClass().getMethod("dashSeparatedMethodName")); - assertThat(this.resolver.resolvePlaceholder("method-name"), - equalTo("dash-separated-method-name")); - } - - @Test - public void underscoreSeparatedMethodName() throws Exception { - when(this.testContext.getTestMethod()).thenReturn( - getClass().getMethod("underscoreSeparatedMethodName")); - assertThat(this.resolver.resolvePlaceholder("method_name"), - equalTo("underscore_separated_method_name")); - } - - @Test - public void camelCaseMethodName() throws Exception { - Method method = getClass().getMethod("camelCaseMethodName"); - when(this.testContext.getTestMethod()).thenReturn(method); - assertThat(this.resolver.resolvePlaceholder("methodName"), - equalTo("camelCaseMethodName")); - } - - @Test - public void stepCount() throws Exception { - assertThat(this.resolver.resolvePlaceholder("step"), equalTo("0")); - } - -} From 7235a49dbc76ba10c9060f6643d15576161c6cb6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Sep 2015 15:05:45 +0100 Subject: [PATCH 0121/1059] Update sample verification to look for snippets in correct location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously each sample was built with both Maven and Gradle and the snippets produced by each build were verified. Now each sample is only built with either Maven or Gradle. This commit updates the snippet verification process to look for snippets in the location that’s appropriate for the sample’s build system. --- .../build/SampleBuildConfigurer.groovy | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7551ecf2c..0a021abe7 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -37,13 +37,15 @@ public class SampleBuildConfigurer { } Task createTask(Project project, Object... dependencies) { - Task verifyIncludes = verifyIncludes(project) + Task verifyIncludes if (new File(this.workingDir, 'build.gradle').isFile()) { - Task gradleBuild = gradleBuild(project, dependencies) + Task gradleBuild = createGradleBuild(project, dependencies) + verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'build/asciidoc')) verifyIncludes.dependsOn gradleBuild } if (new File(this.workingDir, 'pom.xml').isFile()) { - Task mavenBuild = mavenBuild(project, dependencies) + Task mavenBuild = createMavenBuild(project, dependencies) + verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'target/generated-docs')) verifyIncludes.dependsOn(mavenBuild) } Task sampleBuild = project.tasks.create name @@ -53,7 +55,7 @@ public class SampleBuildConfigurer { return sampleBuild } - private Task mavenBuild(Project project, Object... dependencies) { + private Task createMavenBuild(Project project, Object... dependencies) { Task mavenBuild = project.tasks.create("${name}Maven", Exec) mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" @@ -66,7 +68,7 @@ public class SampleBuildConfigurer { return mavenBuild } - private Task gradleBuild(Project project, Object... dependencies) { + private Task createGradleBuild(Project project, Object... dependencies) { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" gradleBuild.group = "Build" @@ -76,19 +78,16 @@ public class SampleBuildConfigurer { return gradleBuild } - private Task verifyIncludes(Project project) { + private Task createVerifyIncludes(Project project, File buildDir) { Task verifyIncludes = project.tasks.create("${name}VerifyIncludes") verifyIncludes.description = "Verifies the includes in the ${name} sample" verifyIncludes << { Map unprocessedIncludes = [:] - [new File(this.workingDir, "build/asciidoc"), - new File(this.workingDir, "target/generated-docs")].each { buildDir -> - buildDir.eachFileRecurse { file -> - if (file.name.endsWith('.html')) { - file.eachLine { line -> - if (line.contains(new File(this.workingDir).absolutePath)) { - unprocessedIncludes.get(file, []).add(line) - } + buildDir.eachFileRecurse { file -> + if (file.name.endsWith('.html')) { + file.eachLine { line -> + if (line.contains(new File(this.workingDir).absolutePath)) { + unprocessedIncludes.get(file, []).add(line) } } } From 51051de4c4664f133f144bbd70d6979f2d24e5dc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Sep 2015 15:07:59 +0100 Subject: [PATCH 0122/1059] Improve contents of generated pom files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the generated pom files were uncustomised and relied on Gradle’s default output. Such pom files are problematic for a couple of reasons: they include test dependencies and they don’t include the metadata that’s required for publishing to Maven Central. This commit customises the generated pom files to include the required metadata and to remove any test dependencies. --- build.gradle | 1 + gradle/publish-maven.gradle | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 gradle/publish-maven.gradle diff --git a/build.gradle b/build.gradle index d2fb8d363..338df543a 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ subprojects { apply plugin: 'propdeps-eclipse' apply plugin: 'propdeps-maven' apply plugin: 'maven' + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" sourceCompatibility = 1.7 targetCompatibility = 1.7 diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle new file mode 100644 index 000000000..ad2d75058 --- /dev/null +++ b/gradle/publish-maven.gradle @@ -0,0 +1,45 @@ +install { + repositories.mavenInstaller { + customizePom(pom, project) + } +} + +def customizePom(pom, gradleProject) { + pom.whenConfigured { generatedPom -> + generatedPom.dependencies.removeAll { dep -> + dep.scope == "test" + } + generatedPom.project { + name = gradleProject.description + description = gradleProject.description + url = "https://github.com/spring-projects/spring-restdocs" + organization { + name = "Spring IO" + url = "http://projects.spring.io/spring-restdocs" + } + licenses { + license { + name "The Apache Software License, Version 2.0" + url "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution "repo" + } + } + scm { + url = "https://github.com/spring-projects/spring-restdocs" + connection = "scm:git:git://github.com/spring-projects/spring-restdocs" + developerConnection = "scm:git:git://github.com/spring-projects/spring-restdocs" + } + developers { + developer { + id = "awilkinson" + name = "Andy Wilkinson" + email = "awilkinson@pivotal.io" + } + } + issueManagement { + system = "GitHub" + url = "https://github.com/spring-projects/spring-restdocs/issues" + } + } + } +} \ No newline at end of file From d80c0f751a4437eda49c3ae8b193bfeb9fc1451c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Sep 2015 15:15:38 +0100 Subject: [PATCH 0123/1059] Cope with different content length due to Windows line endings Windows uses \r\n for line endings whereas Unix-like platforms us \n. This causes the content length of a pretty-printed response to be longer on Windows. --- ...kMvcRestDocumentationIntegrationTests.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a40ad7612..6e015cfde 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -265,16 +265,21 @@ public void preprocessedRequest() throws Exception { .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("Host", "localhost") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Length", "22") - .content(String.format("{%n \"a\" : \"<>\"%n}"))))); + is(snippet() + .withContents( + httpRequest(RequestMethod.GET, "/") + .header("Host", "localhost") + .header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .header("Content-Length", + Integer.toString(prettyPrinted.getBytes().length)) + .content(prettyPrinted)))); } @Test From 570781832e6c009fe15ed7847dcd5c81b3df8ecd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Sep 2015 10:35:09 +0100 Subject: [PATCH 0124/1059] Eclipse metadata for new spring-restdocs-mockmvc project --- .../.settings/org.eclipse.jdt.core.prefs | 305 ++++++++++++++++++ .../.settings/org.eclipse.jdt.ui.prefs | 125 +++++++ 2 files changed, 430 insertions(+) create mode 100644 spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs create mode 100644 spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..b9206d842 --- /dev/null +++ b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,305 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.codeComplete.argumentPrefixes= +org.eclipse.jdt.core.codeComplete.argumentSuffixes= +org.eclipse.jdt.core.codeComplete.fieldPrefixes= +org.eclipse.jdt.core.codeComplete.fieldSuffixes= +org.eclipse.jdt.core.codeComplete.localPrefixes= +org.eclipse.jdt.core.codeComplete.localSuffixes= +org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=1 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=false +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.line_length=90 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.jdt.core.formatter.indentation.size=8 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=90 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true \ No newline at end of file diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..3f14d6355 --- /dev/null +++ b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,125 @@ +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=true +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_functional_interfaces=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.insert_inferred_type_arguments=false +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=false +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=true +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=false +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_anonymous_class_creation=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access_only_if_necessary=false +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_type_arguments=false +cleanup_profile=_Spring Rest Docs Cleanup Conventions +cleanup_settings_version=2 +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_Spring Rest Docs Java Conventions +formatter_settings_version=12 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=true +org.eclipse.jdt.ui.keywordthis=true +org.eclipse.jdt.ui.overrideannotation=true +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=true +sp_cleanup.always_use_this_for_non_static_method_access=true +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=true +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=true +sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=true +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=false +sp_cleanup.use_type_arguments=false \ No newline at end of file From 7b1d9e714cf423da149e0aab355ce6c30bf4170b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Sep 2015 11:36:02 +0100 Subject: [PATCH 0125/1059] Allow preprocessing to be applied to every test Following the reworking of the API, it was no longer possible to apply preprocessing to every test and to still customize the snippets used by that test. This commit adds a new snippets() method to RestDocumentationResultHandler that allows additional snippets to be configured once the result handler has been created. The documentation has been updated to describe how to apply preprocessing to every test and Spring HATEOAS sample has been updated to illustrate the approach. Closes gh-88 --- .../customizing-requests-and-responses.adoc | 27 ++- .../com/example/PreprocessingEveryTest.java | 65 +++++++ ...cessing.java => PreprocessingPerTest.java} | 6 +- .../src/main/resources/application.properties | 1 - .../com/example/notes/ApiDocumentation.java | 164 ++++++++++-------- .../notes/GettingStartedDocumentation.java | 9 +- .../RestDocumentationResultHandler.java | 18 +- .../restdocs/mockmvc/SnippetConfigurer.java | 2 +- 8 files changed, 209 insertions(+), 83 deletions(-) create mode 100644 docs/src/test/java/com/example/PreprocessingEveryTest.java rename docs/src/test/java/com/example/{Preprocessing.java => PreprocessingPerTest.java} (94%) delete mode 100644 samples/rest-notes-spring-hateoas/src/main/resources/application.properties diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 524f3afa8..64830cd9f 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -11,11 +11,36 @@ static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`: [source,java,indent=0] ---- -include::{examples-dir}/com/example/Preprocessing.java[tags=general] +include::{examples-dir}/com/example/PreprocessingPerTest.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. +Alternatively, you may want to apply the same preprocessors to every test. You can do +so by configuring the preprocessors in your `@Before` method and using the +<>: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=setup] +---- +<1> Create the `RestDocumentationResultHandler`, configured to preprocess the request + and response. +<2> Create the `MockMvc` instance, configured to always call the documentation result + handler. + +Then, in each test, the `RestDocumentationResultHandler` can be configured with anything +test-specific. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=use] +---- +<1> Document the links specific to the resource that is being tested +<2> The `perform` call will automatically produce the documentation snippets due to the + use of `alwaysDo` above. + Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See below for further details. diff --git a/docs/src/test/java/com/example/PreprocessingEveryTest.java b/docs/src/test/java/com/example/PreprocessingEveryTest.java new file mode 100644 index 000000000..6199ecb46 --- /dev/null +++ b/docs/src/test/java/com/example/PreprocessingEveryTest.java @@ -0,0 +1,65 @@ +/* + * 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 com.example; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; + +import org.junit.Before; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class PreprocessingEveryTest { + + private WebApplicationContext context; + + private MockMvc mockMvc; + + private RestDocumentationResultHandler document; + + // tag::setup[] + @Before + public void setup() { + this.document = document("{method-name}", // <1> + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .alwaysDo(this.document) // <2> + .build(); + } + // end::setup[] + + public void use() throws Exception { + // tag::use[] + this.document.snippets( // <1> + links(linkWithRel("self").description("Canonical self link"))); + this.mockMvc.perform(get("/")) // <2> + .andExpect(status().isOk()); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/Preprocessing.java b/docs/src/test/java/com/example/PreprocessingPerTest.java similarity index 94% rename from docs/src/test/java/com/example/Preprocessing.java rename to docs/src/test/java/com/example/PreprocessingPerTest.java index dbb837137..6f0ab77ff 100644 --- a/docs/src/test/java/com/example/Preprocessing.java +++ b/docs/src/test/java/com/example/PreprocessingPerTest.java @@ -26,18 +26,18 @@ import org.springframework.test.web.servlet.MockMvc; -public class Preprocessing { +public class PreprocessingPerTest { private MockMvc mockMvc; public void general() throws Exception { - // tag::general[] + // tag::preprocessing[] this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> preprocessResponse(prettyPrint()))); // <2> - // end::general[] + // end::preprocessing[] } } diff --git a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties b/samples/rest-notes-spring-hateoas/src/main/resources/application.properties deleted file mode 100644 index 8e06a8284..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.jackson.serialization.indent_output: true \ No newline at end of file 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 8a0978df5..81a7fef7a 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 @@ -18,13 +18,16 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -48,6 +51,7 @@ import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -66,6 +70,8 @@ public class ApiDocumentation { @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + + private RestDocumentationResultHandler document; @Autowired private NoteRepository noteRepository; @@ -83,12 +89,25 @@ public class ApiDocumentation { @Before public void setUp() { + this.document = document("{method-name}", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(this.document) + .build(); } @Test public void errorExample() throws Exception { + this.document.snippets(responseFields( + 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"))); + this.mockMvc .perform(get("/error") .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) @@ -100,26 +119,20 @@ public void errorExample() throws Exception { .andExpect(jsonPath("error", is("Bad Request"))) .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example", - responseFields( - 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")))); + .andExpect(jsonPath("path", is(notNullValue()))); } @Test public void indexExample() throws Exception { + this.document.snippets( + links( + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>")), + responseFields( + fieldWithPath("_links").description("<> to other resources"))); + this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index-example", - links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")), - responseFields( - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(status().isOk()); } @Test @@ -131,12 +144,13 @@ public void notesListExample() throws Exception { createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); + + this.document.snippets( + responseFields( + fieldWithPath("_embedded.notes").description("An array of <>"))); this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()) - .andDo(document("notes-list-example", - responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + .andExpect(status().isOk()); } @Test @@ -157,16 +171,17 @@ public void notesCreateExample() throws Exception { note.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NoteInput.class); + + this.document.snippets( + requestFields( + fields.withPath("title").description("The title of the note"), + fields.withPath("body").description("The body of the note"), + fields.withPath("tags").description("An array of tag resource URIs"))); this.mockMvc.perform( post("/notes").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andDo(document("notes-create-example", - requestFields( - fields.withPath("title").description("The title of the note"), - fields.withPath("body").description("The body of the note"), - fields.withPath("tags").description("An array of tag resource URIs")))); + .andExpect(status().isCreated()); } @Test @@ -192,21 +207,22 @@ public void noteGetExample() throws Exception { this.objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); + + this.document.snippets( + links( + linkWithRel("self").description("This <>"), + linkWithRel("note-tags").description("This note's <>")), + responseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("<> to other resources"))); this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("title", is(note.get("title")))) .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", - links( - linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); } @@ -218,12 +234,13 @@ public void tagsListExample() throws Exception { createTag("REST"); createTag("Hypermedia"); createTag("HTTP"); + + this.document.snippets( + responseFields( + fieldWithPath("_embedded.tags").description("An array of <>"))); this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()) - .andDo(document("tags-list-example", - responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + .andExpect(status().isOk()); } @Test @@ -232,14 +249,15 @@ public void tagsCreateExample() throws Exception { tag.put("name", "REST"); ConstrainedFields fields = new ConstrainedFields(TagInput.class); + + this.document.snippets( + requestFields( + fields.withPath("name").description("The name of the tag"))); this.mockMvc.perform( post("/tags").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andDo(document("tags-create-example", - requestFields( - fields.withPath("name").description("The name of the tag")))); + .andExpect(status().isCreated()); } @Test @@ -275,23 +293,24 @@ public void noteUpdateExample() throws Exception { noteUpdate.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class); + + this.document.snippets( + requestFields( + fields.withPath("title") + .description("The title of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("body") + .description("The body of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("tags") + .description("An array of tag resource URIs"))); this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("note-update-example", - requestFields( - fields.withPath("title") - .description("The title of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("body") - .description("The body of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("tags") - .description("An array of tag resource URIs")))); + .andExpect(status().isNoContent()); } @Test @@ -305,17 +324,18 @@ public void tagGetExample() throws Exception { this.objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); + + this.document.snippets( + links( + linkWithRel("self").description("This <>"), + linkWithRel("tagged-notes").description("The <> that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> to other resources"))); this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example", - links( - linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(jsonPath("name", is(tag.get("name")))); } @Test @@ -334,15 +354,15 @@ public void tagUpdateExample() throws Exception { tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); + + this.document.snippets( + requestFields( + fields.withPath("name").description("The name of the tag"))); this.mockMvc.perform( patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("tag-update-example", - requestFields( - fields.withPath("name") - .description("The name of the tag")))); + .andExpect(status().isNoContent()); } private void createNote(String title, String body) { diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index b13657291..60ba09692 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,11 +19,14 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -72,7 +75,9 @@ public class GettingStartedDocumentation { public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")) + .alwaysDo(document("{method-name}/{step}/", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()))) .build(); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 2a7e4ab4c..32834c9ef 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -79,7 +79,7 @@ public class RestDocumentationResultHandler implements ResultHandler { this.identifier = identifier; this.requestPreprocessor = requestPreprocessor; this.responsePreprocessor = responsePreprocessor; - this.snippets = Arrays.asList(snippets); + this.snippets = new ArrayList<>(Arrays.asList(snippets)); } @Override @@ -102,11 +102,23 @@ public void handle(MvcResult result) throws Exception { } } + /** + * Adds the given {@code snippets} such that that are documented when this result + * handler is called. + * + * @param snippets the snippets to add + * @return this {@code ResultDocumentationResultHandler} + */ + public RestDocumentationResultHandler snippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + return this; + } + @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { List combinedSnippets = new ArrayList<>((List) result - .getRequest() - .getAttribute("org.springframework.restdocs.defaultSnippets")); + .getRequest().getAttribute( + "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index ace722ea1..ba874f42b 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -64,7 +64,7 @@ public SnippetConfigurer withEncoding(String encoding) { void apply(MockHttpServletRequest request) { ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) .setEncoding(this.snippetEncoding); - request.setAttribute("org.springframework.restdocs.defaultSnippets", + request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", this.defaultSnippets); } From de095eb9fe1cb505f6a9a82cd8a621be3f66e508 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Sep 2015 13:20:03 +0100 Subject: [PATCH 0126/1059] Add package-info.java to each package Closes gh-109 --- .../restdocs/constraints/package-info.java | 20 ++++++++++++++++++ .../restdocs/curl/package-info.java | 20 ++++++++++++++++++ .../restdocs/http/package-info.java | 21 +++++++++++++++++++ .../restdocs/hypermedia/package-info.java | 20 ++++++++++++++++++ .../restdocs/operation/package-info.java | 21 +++++++++++++++++++ .../operation/preprocess/package-info.java | 20 ++++++++++++++++++ .../restdocs/package-info.java | 20 ++++++++++++++++++ .../restdocs/payload/package-info.java | 20 ++++++++++++++++++ .../restdocs/request/package-info.java | 20 ++++++++++++++++++ .../restdocs/snippet/package-info.java | 20 ++++++++++++++++++ .../templates/mustache/package-info.java | 20 ++++++++++++++++++ .../restdocs/templates/package-info.java | 20 ++++++++++++++++++ .../{util => }/IterableEnumeration.java | 6 +++--- .../MockMvcOperationRequestFactory.java | 2 +- .../RestDocumentationResultHandler.java | 2 +- .../restdocs/mockmvc/package-info.java | 20 ++++++++++++++++++ 16 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{util => }/IterableEnumeration.java (90%) create mode 100644 spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java new file mode 100644 index 000000000..1692913c2 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting a RESTful API's constraints. + */ +package org.springframework.restdocs.constraints; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java new file mode 100644 index 000000000..124090985 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the curl command required to make a request to a RESTful API. + */ +package org.springframework.restdocs.curl; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java new file mode 100644 index 000000000..258da97d6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Documenting the HTTP request sent to a RESTful API and the HTTP response that is + * returned. + */ +package org.springframework.restdocs.http; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java new file mode 100644 index 000000000..a792c083c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting a RESTful API that uses hypermedia. + */ +package org.springframework.restdocs.hypermedia; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java new file mode 100644 index 000000000..2321f69e1 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Operation API that describes a request that was sent and the response that was + * received when calling a RESTful API. + */ +package org.springframework.restdocs.operation; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java new file mode 100644 index 000000000..2a50da0c0 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support for preprocessing an operation prior to it being documented. + */ +package org.springframework.restdocs.operation.preprocess; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java new file mode 100644 index 000000000..c237f39a3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Core Spring REST Docs classes. + */ +package org.springframework.restdocs; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java new file mode 100644 index 000000000..1e164515d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the payload of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.payload; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java new file mode 100644 index 000000000..9e78ff299 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting query and path parameters of requests sent to a RESTful API. + */ +package org.springframework.restdocs.request; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java new file mode 100644 index 000000000..1e8540b92 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Snippet generation. + */ +package org.springframework.restdocs.snippet; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java new file mode 100644 index 000000000..0b4246df3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * JMustache-based implementation of the template API. + */ +package org.springframework.restdocs.templates.mustache; \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java new file mode 100644 index 000000000..7f9b1d103 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Template API used to render documentation snippets. + */ +package org.springframework.restdocs.templates; \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/util/IterableEnumeration.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java similarity index 90% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/util/IterableEnumeration.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java index 663cef47b..5e7a7580e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/util/IterableEnumeration.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc.util; +package org.springframework.restdocs.mockmvc; import java.util.Enumeration; import java.util.Iterator; @@ -26,7 +26,7 @@ * * @param the type of the Enumeration's contents */ -public final class IterableEnumeration implements Iterable { +final class IterableEnumeration implements Iterable { private final Enumeration enumeration; @@ -63,7 +63,7 @@ public void remove() { * @param enumeration The enumeration to expose as an {@code Iterable} * @return the iterable */ - public static Iterable iterable(Enumeration enumeration) { + static Iterable iterable(Enumeration enumeration) { return new IterableEnumeration(enumeration); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 70d4351ee..d3236e084 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.mockmvc.util.IterableEnumeration.iterable; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; import java.io.IOException; import java.io.PrintWriter; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 32834c9ef..35d65436f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.mockmvc.util.IterableEnumeration.iterable; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; import java.util.ArrayList; import java.util.Arrays; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java new file mode 100644 index 000000000..7ba1cf227 --- /dev/null +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Core classes for using Spring REST Docs with Spring Test's MockMvc. + */ +package org.springframework.restdocs.mockmvc; \ No newline at end of file From c2da4c912aee58c08c9615466b0bf44d5c1e79a9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 10:30:50 +0100 Subject: [PATCH 0127/1059] Make the built-in snippets more extensible The commit opens up all of the default snippets so that it's easier to extend them and modify their behaviour. All of the built-in snippets are now public with protected constructors. Also, where appropriate the models used by the snippets have been made more fine-grained. Closes gh-73 --- .../restdocs/curl/CurlRequestSnippet.java | 34 +++++++++++------ .../restdocs/http/HttpRequestSnippet.java | 22 ++++++++--- .../restdocs/http/HttpResponseSnippet.java | 19 ++++++++-- .../hypermedia/HypermediaDocumentation.java | 8 ++-- .../restdocs/hypermedia/LinksSnippet.java | 30 ++++++++++++--- .../payload/AbstractFieldsSnippet.java | 33 +++++++++++++++-- .../payload/PayloadDocumentation.java | 4 +- .../payload/RequestFieldsSnippet.java | 27 +++++++++++--- .../payload/ResponseFieldsSnippet.java | 28 +++++++++++--- .../request/AbstractParametersSnippet.java | 21 ++++++++++- .../request/PathParametersSnippet.java | 32 ++++++++++++---- .../request/RequestDocumentation.java | 4 +- .../request/RequestParametersSnippet.java | 37 +++++++++++++------ .../restdocs/snippet/TemplatedSnippet.java | 8 ++++ .../templates/default-curl-request.snippet | 2 +- .../hypermedia/LinksSnippetTests.java | 6 +-- .../payload/RequestFieldsSnippetTests.java | 4 +- .../payload/ResponseFieldsSnippetTests.java | 4 +- .../request/PathParametersSnippetTests.java | 4 +- .../RequestParametersSnippetTests.java | 4 +- .../curl-request-with-title.snippet | 2 +- 21 files changed, 249 insertions(+), 84 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4544c13aa..ef04fd83c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -36,31 +36,43 @@ * A {@link Snippet} that documents the curl command for a request. * * @author Andy Wilkinson + * @see CurlDocumentation#curlRequest() + * @see CurlDocumentation#curlRequest(Map) */ -class CurlRequestSnippet extends TemplatedSnippet { +public class CurlRequestSnippet extends TemplatedSnippet { - CurlRequestSnippet() { + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected CurlRequestSnippet() { this(null); } - CurlRequestSnippet(Map attributes) { + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected CurlRequestSnippet(Map attributes) { super("curl-request", attributes); } @Override - public Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) throws IOException { Map model = new HashMap(); - model.put("arguments", getCurlCommandArguments(operation)); + model.put("url", getUrl(operation)); + model.put("options", getOptions(operation)); return model; } - private String getCurlCommandArguments(Operation operation) throws IOException { + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getOptions(Operation operation) throws IOException { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); - printer.print("'"); - printer.print(operation.getRequest().getUri()); - printer.print("'"); - writeOptionToIncludeHeadersInOutput(printer); writeHttpMethodIfNecessary(operation.getRequest(), printer); writeHeaders(operation.getRequest(), printer); @@ -72,7 +84,7 @@ private String getCurlCommandArguments(Operation operation) throws IOException { } private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { - writer.print(" -i"); + writer.print("-i"); } private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index fd30849a2..b98e33c8b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -39,21 +39,32 @@ * A {@link Snippet} that documents an HTTP request. * * @author Andy Wilkinson + * @see HttpDocumentation#httpRequest() + * @see HttpDocumentation#httpRequest(Map) */ -class HttpRequestSnippet extends TemplatedSnippet { +public class HttpRequestSnippet extends TemplatedSnippet { private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - HttpRequestSnippet() { + /** + * Creates a new {@code HttpRequestSnippet} with no additional attributes. + */ + protected HttpRequestSnippet() { this(null); } - HttpRequestSnippet(Map attributes) { + /** + * Creates a new {@code HttpRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpRequestSnippet(Map attributes) { super("http-request", attributes); } @Override - public Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) throws IOException { Map model = new HashMap(); model.put("method", operation.getRequest().getMethod()); model.put( @@ -61,8 +72,7 @@ public Map createModel(Operation operation) throws IOException { operation.getRequest().getUri().getRawPath() + (StringUtils.hasText(operation.getRequest().getUri() .getRawQuery()) ? "?" - + operation.getRequest().getUri().getRawQuery() - : "")); + + operation.getRequest().getUri().getRawQuery() : "")); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 7f4f6b658..07f9a5b0d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -33,19 +33,30 @@ * A {@link Snippet} that documents an HTTP response. * * @author Andy Wilkinson + * @see HttpDocumentation#httpResponse() + * @see HttpDocumentation#httpResponse(Map) */ -class HttpResponseSnippet extends TemplatedSnippet { +public class HttpResponseSnippet extends TemplatedSnippet { - HttpResponseSnippet() { + /** + * Creates a new {@code HttpResponseSnippet} with no additional attributes. + */ + protected HttpResponseSnippet() { this(null); } - HttpResponseSnippet(Map attributes) { + /** + * Creates a new {@code HttpResponseSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpResponseSnippet(Map attributes) { super("http-response", attributes); } @Override - public Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) throws IOException { OperationResponse response = operation.getResponse(); HttpStatus status = response.getStatus(); Map model = new HashMap(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 52c0cccbc..968f5dad4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -67,8 +67,8 @@ public static Snippet links(LinkDescriptor... descriptors) { */ public static Snippet links(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), attributes, - Arrays.asList(descriptors)); + return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors), + attributes); } /** @@ -98,8 +98,8 @@ public static Snippet links(LinkExtractor linkExtractor, */ public static Snippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, attributes, - Arrays.asList(descriptors)); + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), + attributes); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 1e01716c6..551d83ab9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -37,8 +37,12 @@ * A {@link Snippet} that documents a RESTful resource's links. * * @author Andy Wilkinson + * @see HypermediaDocumentation#links(LinkDescriptor...) + * @see HypermediaDocumentation#links(LinkExtractor, LinkDescriptor...) + * @see HypermediaDocumentation#links(Map, LinkDescriptor...) + * @see HypermediaDocumentation#links(LinkExtractor, Map, LinkDescriptor...) */ -class LinksSnippet extends TemplatedSnippet { +public class LinksSnippet extends TemplatedSnippet { private final Map descriptorsByRel = new LinkedHashMap<>(); @@ -46,12 +50,28 @@ class LinksSnippet extends TemplatedSnippet { private final LinkExtractor linkExtractor; - LinksSnippet(LinkExtractor linkExtractor, List descriptors) { - this(linkExtractor, null, descriptors); + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { + this(linkExtractor, descriptors, null); } - LinksSnippet(LinkExtractor linkExtractor, Map attributes, - List descriptors) { + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. The + * given {@code attributes} will be included in the model during template rendering. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param attributes the additional attributes + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + Map attributes) { super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index ec93b8d1f..802d34c15 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -30,8 +30,8 @@ import org.springframework.util.StringUtils; /** - * A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's - * request or response fields. + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response fields. * * @author Andreas Evers * @author Andy Wilkinson @@ -40,8 +40,18 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private List fieldDescriptors; - AbstractFieldsSnippet(String type, Map attributes, - List descriptors) { + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); @@ -106,8 +116,23 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { } } + /** + * Returns the content type of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content type + */ protected abstract MediaType getContentType(Operation operation); + /** + * Returns the content of the request or response extracted form the given + * {@code operation}. + * + * @param operation The operation + * @return The content + * @throws IOException if the content cannot be extracted + */ protected abstract byte[] getContent(Operation operation) throws IOException; } \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 72b0494a5..c32df1ba0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -136,7 +136,7 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { */ public static Snippet requestFields(Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors)); + return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } /** @@ -177,7 +177,7 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { */ public static Snippet responseFields(Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors)); + return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 1f72d4a30..130357faf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -28,15 +28,32 @@ * A {@link Snippet} that documents the fields in a request. * * @author Andy Wilkinson + * @see PayloadDocumentation#requestFields(FieldDescriptor...) + * @see PayloadDocumentation#requestFields(Map, FieldDescriptor...) */ -class RequestFieldsSnippet extends AbstractFieldsSnippet { +public class RequestFieldsSnippet extends AbstractFieldsSnippet { - RequestFieldsSnippet(List descriptors) { - this(null, descriptors); + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected RequestFieldsSnippet(List descriptors) { + this(descriptors, null); } - RequestFieldsSnippet(Map attributes, List descriptors) { - super("request", attributes, descriptors); + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestFieldsSnippet(List descriptors, + Map attributes) { + super("request", descriptors, attributes); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 4cd92ceaf..4e829cad8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -27,16 +27,32 @@ * A {@link Snippet} that documents the fields in a response. * * @author Andy Wilkinson + * @see PayloadDocumentation#responseFields(FieldDescriptor...) + * @see PayloadDocumentation#responseFields(Map, FieldDescriptor...) */ -class ResponseFieldsSnippet extends AbstractFieldsSnippet { +public class ResponseFieldsSnippet extends AbstractFieldsSnippet { - ResponseFieldsSnippet(List descriptors) { - this(null, descriptors); + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected ResponseFieldsSnippet(List descriptors) { + this(descriptors, null); } - ResponseFieldsSnippet(Map attributes, - List descriptors) { - super("response", attributes, descriptors); + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseFieldsSnippet(List descriptors, + Map attributes) { + super("response", descriptors, attributes); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index df0411dc4..0a89f4c26 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -30,12 +30,29 @@ import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Assert; -abstract class AbstractParametersSnippet extends TemplatedSnippet { +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document parameters from a request sent to a RESTful resource. + * + * @author Andreas Evers + * @author Andy Wilkinson + */ +public abstract class AbstractParametersSnippet extends TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); + /** + * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with + * the given {@code snippetName} that will document parameters using the given + * {@code descriptors}. The given {@code attributes} will be included in the model + * during template rendering. + * + * @param snippetName The snippet name + * @param descriptors The descriptors + * @param attributes The additional attributes + */ protected AbstractParametersSnippet(String snippetName, - Map attributes, List descriptors) { + List descriptors, Map attributes) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getName()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 99c3c7658..4082d2822 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -33,28 +33,44 @@ * A {@link Snippet} that documents the path parameters supported by a RESTful resource. * * @author Andy Wilkinson + * @see RequestDocumentation#pathParameters(ParameterDescriptor...) + * @see RequestDocumentation#pathParameters(Map, ParameterDescriptor...) */ -class PathParametersSnippet extends AbstractParametersSnippet { +public class PathParametersSnippet extends AbstractParametersSnippet { private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); - PathParametersSnippet(List descriptors) { - this(null, descriptors); + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. + * + * @param descriptors the parameter descriptors + */ + protected PathParametersSnippet(List descriptors) { + this(descriptors, null); } - PathParametersSnippet(Map attributes, - List descriptors) { - super("path-parameters", attributes, descriptors); + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected PathParametersSnippet(List descriptors, + Map attributes) { + super("path-parameters", descriptors, attributes); } @Override protected Map createModel(Operation operation) throws IOException { Map model = super.createModel(operation); - model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation))); + model.put("path", removeQueryStringIfPresent(extractUrlTemplate(operation))); return model; } - private String remoteQueryStringIfPresent(String urlTemplate) { + private String removeQueryStringIfPresent(String urlTemplate) { int index = urlTemplate.indexOf('?'); if (index == -1) { return urlTemplate; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 9f3887f2e..1a0510771 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -71,7 +71,7 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { */ public static Snippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(attributes, Arrays.asList(descriptors)); + return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } /** @@ -100,7 +100,7 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { */ public static Snippet requestParameters(Map attributes, ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(attributes, Arrays.asList(descriptors)); + return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 386fcd1a6..dbe4e372c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -20,32 +20,45 @@ import java.util.Map; import java.util.Set; -import javax.servlet.ServletRequest; - import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.web.bind.annotation.RequestParam; /** * A {@link Snippet} that documents the request parameters supported by a RESTful * resource. *

    - * Request parameters are sent as part of the query string or as posted from data. + * Request parameters are sent as part of the query string or as POSTed form data. * * @author Andy Wilkinson - * @see ServletRequest#getParameterMap() - * @see RequestParam + * @see OperationRequest#getParameters() + * @see RequestDocumentation#requestParameters(ParameterDescriptor...) + * @see RequestDocumentation#requestParameters(Map, ParameterDescriptor...) */ -class RequestParametersSnippet extends AbstractParametersSnippet { +public class RequestParametersSnippet extends AbstractParametersSnippet { - RequestParametersSnippet(List descriptors) { - this(null, descriptors); + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. + * + * @param descriptors the parameter descriptors + */ + protected RequestParametersSnippet(List descriptors) { + this(descriptors, null); } - RequestParametersSnippet(Map attributes, - List descriptors) { - super("request-parameters", attributes, descriptors); + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected RequestParametersSnippet(List descriptors, + Map attributes) { + super("request-parameters", descriptors, attributes); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 4e0cf65d0..87050ce58 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -38,6 +38,14 @@ public abstract class TemplatedSnippet implements Snippet { private final String snippetName; + /** + * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given + * {@code snippetName}. The given {@code attributes} will be included in the model + * during rendering of the template. + * + * @param snippetName The name of the snippet + * @param attributes The additional attributes + */ protected TemplatedSnippet(String snippetName, Map attributes) { this.snippetName = snippetName; if (attributes != null) { diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet index 8eaef9e12..81fe65eeb 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet @@ -1,4 +1,4 @@ [source,bash] ---- -$ curl {{arguments}} +$ curl {{url}} {{options}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index f5a514809..24bd7ec76 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -154,10 +154,10 @@ public void linksWithCustomAttributes() throws IOException { new FileSystemResource( "src/test/resources/custom-snippet-templates/links-with-title.snippet")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), attributes(key("title").value( - "Title for the links")), Arrays.asList( + new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))) + new LinkDescriptor("b").description("two")), attributes(key("title").value( + "Title for the links"))) .document(new OperationBuilder("links-with-custom-attributes", this.snippet.getOutputDirectory()).attribute( TemplateEngine.class.getName(), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 35168549b..819dd592e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -170,8 +170,8 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("request-fields")).thenReturn( snippetResource("request-fields-with-title")); - new RequestFieldsSnippet(attributes(key("title").value("Custom title")), - Arrays.asList(fieldWithPath("a").description("one"))) + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))) .document(new OperationBuilder("request-fields-with-custom-attributes", this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 16f5a8533..e8459d7f8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -139,8 +139,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); when(resolver.resolveTemplateResource("response-fields")).thenReturn( snippetResource("response-fields-with-title")); - new ResponseFieldsSnippet(attributes(key("title").value("Custom title")), - Arrays.asList(fieldWithPath("a").description("one"))) + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))) .document(new OperationBuilder("response-fields-with-custom-attributes", this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 5c5657c87..5d858d936 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -147,11 +147,11 @@ public void pathParametersWithCustomAttributes() throws IOException { when(resolver.resolveTemplateResource("path-parameters")).thenReturn( snippetResource("path-parameters-with-title")); new PathParametersSnippet( - attributes(key("title").value("The title")), Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) + .description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) .document(new OperationBuilder("path-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 5a071ffd0..330d411c5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -131,11 +131,11 @@ public void requestParametersWithCustomAttributes() throws IOException { when(resolver.resolveTemplateResource("request-parameters")).thenReturn( snippetResource("request-parameters-with-title")); new RequestParametersSnippet( - attributes(key("title").value("The title")), Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) + .description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) .document(new OperationBuilder( "request-parameters-with-custom-attributes", this.snippet .getOutputDirectory()) diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet index b53c203cf..fa5ee3f4b 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet @@ -1,5 +1,5 @@ [source,bash] .{{title}} ---- -$ curl {{arguments}} +$ curl {{url}} {{options}} ---- \ No newline at end of file From fece9547ea607aef3b5b839517c5dccb2b4d9a3f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 11:20:28 +0100 Subject: [PATCH 0128/1059] Automatically align version of samples' dependency with main version This commit enhances the tasks that build the samples to automatically update the version of their spring-restdocs dependency to match the version of the main project. --- .../build/SampleBuildConfigurer.groovy | 24 +++++++++++++++++++ samples/rest-notes-spring-data-rest/pom.xml | 5 ++-- .../rest-notes-spring-hateoas/build.gradle | 13 +++++----- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 0a021abe7..96db02084 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -20,6 +20,7 @@ import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.Copy import org.gradle.api.tasks.GradleBuild public class SampleBuildConfigurer { @@ -65,6 +66,13 @@ public class SampleBuildConfigurer { "${System.env.MAVEN_HOME}/bin/mvn${suffix}" : "mvn${suffix}", 'clean', 'package'] mavenBuild.dependsOn dependencies + + mavenBuild.doFirst { + replaceVersion(new File(this.workingDir, 'pom.xml'), + '.*', + "${project.version}") + } + return mavenBuild } @@ -75,9 +83,25 @@ public class SampleBuildConfigurer { gradleBuild.dir = this.workingDir gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies + + gradleBuild.doFirst { + replaceVersion(new File(this.workingDir, 'build.gradle'), + "springRestdocsVersion = '.*'", + "springRestdocsVersion = '${project.version}'") + } + return gradleBuild } + private void replaceVersion(File target, String pattern, String replacement) { + def lines = target.readLines() + target.withWriter { writer -> + lines.each { line -> + writer.println(line.replaceAll(pattern, replacement)) + } + } + } + private Task createVerifyIncludes(Project project, File buildDir) { Task verifyIncludes = project.tasks.create("${name}VerifyIncludes") verifyIncludes.description = "Verifies the includes in the ${name} sample" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 7e668af49..d478c005c 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,6 +18,7 @@ UTF-8 1.7 + 1.0.0.BUILD-SNAPSHOT @@ -49,7 +50,7 @@ org.springframework.restdocs spring-restdocs-mockmvc - 1.0.0.BUILD-SNAPSHOT + ${spring-restdocs.version} test @@ -124,4 +125,4 @@ - \ No newline at end of file + diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a8041b902..7ab7a4045 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -26,6 +26,11 @@ group = 'com.example' sourceCompatibility = 1.7 targetCompatibility = 1.7 +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.0.0.BUILD-SNAPSHOT' +} + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-hateoas' @@ -35,11 +40,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:1.0.0.BUILD-SNAPSHOT' -} - -ext { - snippetsDir = file('build/generated-snippets') + testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" } test { @@ -61,4 +62,4 @@ jar { } eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } \ No newline at end of file +cleanEclipseJdt.onlyIf { false } From 875dde3b6f3e1b7a0108d6189149b5cc1b3c8d0d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 11:40:57 +0100 Subject: [PATCH 0129/1059] Polish exception handling in snippet implementations --- .../restdocs/curl/CurlRequestSnippet.java | 11 +++--- .../restdocs/http/HttpRequestSnippet.java | 11 +++--- .../restdocs/http/HttpResponseSnippet.java | 3 +- .../restdocs/hypermedia/LinksSnippet.java | 10 ++++-- .../payload/AbstractFieldsSnippet.java | 31 ++++++++++------ .../request/AbstractParametersSnippet.java | 3 +- .../request/PathParametersSnippet.java | 3 +- .../snippet/ModelCreationException.java | 35 +++++++++++++++++++ .../restdocs/snippet/TemplatedSnippet.java | 13 +++++-- 9 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index ef04fd83c..018cb0f1a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.curl; -import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; @@ -59,7 +58,7 @@ protected CurlRequestSnippet(Map attributes) { } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { Map model = new HashMap(); model.put("url", getUrl(operation)); model.put("options", getOptions(operation)); @@ -70,7 +69,7 @@ private String getUrl(Operation operation) { return String.format("'%s'", operation.getRequest().getUri()); } - private String getOptions(Operation operation) throws IOException { + private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeOptionToIncludeHeadersInOutput(printer); @@ -101,8 +100,7 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { } } - private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) - throws IOException { + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { for (OperationRequestPart part : request.getParts()) { writer.printf(" -F '%s=", part.getName()); if (!StringUtils.hasText(part.getSubmittedFileName())) { @@ -120,8 +118,7 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) } } - private void writeContent(OperationRequest request, PrintWriter writer) - throws IOException { + private void writeContent(OperationRequest request, PrintWriter writer) { if (request.getContent().length > 0) { writer.print(String.format(" -d '%s'", new String(request.getContent()))); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index b98e33c8b..322474d60 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.http; -import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -64,7 +63,7 @@ protected HttpRequestSnippet(Map attributes) { } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { Map model = new HashMap(); model.put("method", operation.getRequest().getMethod()); model.put( @@ -104,7 +103,7 @@ private List> getHeaders(OperationRequest request) { return headers; } - private String getRequestBody(OperationRequest request) throws IOException { + private String getRequestBody(OperationRequest request) { StringWriter httpRequest = new StringWriter(); PrintWriter writer = new PrintWriter(httpRequest); if (request.getContent().length > 0) { @@ -131,8 +130,7 @@ private boolean isPutOrPost(OperationRequest request) { || HttpMethod.POST.equals(request.getMethod()); } - private void writeParts(OperationRequest request, PrintWriter writer) - throws IOException { + private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); for (Entry> parameter : request.getParameters().entrySet()) { for (String value : parameter.getValue()) { @@ -153,8 +151,7 @@ private void writePartBoundary(PrintWriter writer) { writer.printf("--%s%n", MULTIPART_BOUNDARY); } - private void writePart(OperationRequestPart part, PrintWriter writer) - throws IOException { + private void writePart(OperationRequestPart part, PrintWriter writer) { writePart(part.getName(), new String(part.getContent()), part.getHeaders() .getContentType(), writer); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 07f9a5b0d..b3483a548 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.http; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -56,7 +55,7 @@ protected HttpResponseSnippet(Map attributes) { } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); HttpStatus status = response.getStatus(); Map model = new HashMap(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 551d83ab9..a182d8df6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -28,6 +28,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -85,9 +86,14 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); - validate(this.linkExtractor.extractLinks(response)); + try { + validate(this.linkExtractor.extractLinks(response)); + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } Map model = new HashMap<>(); model.put("links", createLinksModel()); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 802d34c15..33aa114cb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -24,6 +24,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Assert; @@ -61,16 +62,8 @@ protected AbstractFieldsSnippet(String type, List descriptors, } @Override - protected Map createModel(Operation operation) throws IOException { - MediaType contentType = getContentType(operation); - ContentHandler contentHandler; - if (contentType != null - && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(getContent(operation)); - } - else { - contentHandler = new JsonContentHandler(getContent(operation)); - } + protected Map createModel(Operation operation) { + ContentHandler contentHandler = getContentHandler(operation); validateFieldDocumentation(contentHandler); @@ -89,6 +82,24 @@ protected Map createModel(Operation operation) throws IOExceptio return model; } + private ContentHandler getContentHandler(Operation operation) { + MediaType contentType = getContentType(operation); + ContentHandler contentHandler; + try { + if (contentType != null + && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { + contentHandler = new XmlContentHandler(getContent(operation)); + } + else { + contentHandler = new JsonContentHandler(getContent(operation)); + } + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + return contentHandler; + } + private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 0a89f4c26..bafbdd37e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.request; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -62,7 +61,7 @@ protected AbstractParametersSnippet(String snippetName, } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { verifyParameterDescriptors(operation); Map model = new HashMap<>(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 4082d2822..b67a9985a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.request; -import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -64,7 +63,7 @@ protected PathParametersSnippet(List descriptors, } @Override - protected Map createModel(Operation operation) throws IOException { + protected Map createModel(Operation operation) { Map model = super.createModel(operation); model.put("path", removeQueryStringIfPresent(extractUrlTemplate(operation))); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java new file mode 100644 index 000000000..db93a778a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java @@ -0,0 +1,35 @@ +package org.springframework.restdocs.snippet; + +import org.springframework.restdocs.operation.Operation; + +/** + * An exception that can be thrown by a {@link TemplatedSnippet} to indicate that a + * failure has occurred during model creation. + * + * @author Andy Wilkinson + * @see TemplatedSnippet#createModel(Operation) + */ +@SuppressWarnings("serial") +public class ModelCreationException extends RuntimeException { + + /** + * Creates a new {@code ModelCreationException} with the given {@code cause}. + * + * @param cause the cause + */ + public ModelCreationException(Throwable cause) { + super(cause); + } + + /** + * Creates a new {@code ModelCreationException} with the given {@code message} and + * {@code cause}. + * + * @param message the message + * @param cause the cause + */ + public ModelCreationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 87050ce58..1e7dcc1df 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -69,7 +69,16 @@ public void document(Operation operation) throws IOException { } } - protected abstract Map createModel(Operation operation) - throws IOException; + /** + * Create the model that should be used during template rendering to document the + * given {@code operation}. Any additional attributes that were supplied when this + * {@code TemplatedSnippet} were created will be automatically added to the model + * prior to rendering. + * + * @param operation The operation + * @return the model + * @throws ModelCreationException if model creation fails + */ + protected abstract Map createModel(Operation operation); } \ No newline at end of file From d09950ecaac76c327fd15c7fa93a0275135f265b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 12:29:36 +0100 Subject: [PATCH 0130/1059] Polish MockMvcOperationRequestFactory --- .../MockMvcOperationRequestFactory.java | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index d3236e084..4d1c4a9f9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -86,37 +86,57 @@ public OperationRequest createOperationRequest(MockHttpServletRequest mockReques private List extractParts(MockHttpServletRequest servletRequest) throws IOException, ServletException { List parts = new ArrayList<>(); + parts.addAll(extractServletRequestParts(servletRequest)); + if (servletRequest instanceof MockMultipartHttpServletRequest) { + parts.addAll(extractMultipartRequestParts((MockMultipartHttpServletRequest) servletRequest)); + } + return parts; + } + + private List extractServletRequestParts( + MockHttpServletRequest servletRequest) throws IOException, ServletException { + List parts = new ArrayList<>(); for (Part part : servletRequest.getParts()) { - HttpHeaders partHeaders = extractHeaders(part); - List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); - if (part.getContentType() != null && contentTypeHeader == null) { - partHeaders - .setContentType(MediaType.parseMediaType(part.getContentType())); - } - parts.add(new StandardOperationRequestPart(part.getName(), StringUtils - .hasText(part.getSubmittedFileName()) ? part.getSubmittedFileName() - : null, FileCopyUtils.copyToByteArray(part.getInputStream()), - partHeaders)); + parts.add(createOperationRequestPart(part)); } - if (servletRequest instanceof MockMultipartHttpServletRequest) { - for (Entry> entry : ((MockMultipartHttpServletRequest) servletRequest) - .getMultiFileMap().entrySet()) { - for (MultipartFile file : entry.getValue()) { - HttpHeaders partHeaders = new HttpHeaders(); - if (StringUtils.hasText(file.getContentType())) { - partHeaders.setContentType(MediaType.parseMediaType(file - .getContentType())); - } - parts.add(new StandardOperationRequestPart(file.getName(), - StringUtils.hasText(file.getOriginalFilename()) ? file - .getOriginalFilename() : null, file.getBytes(), - partHeaders)); - } + return parts; + } + + private StandardOperationRequestPart createOperationRequestPart(Part part) + throws IOException { + HttpHeaders partHeaders = extractHeaders(part); + List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); + if (part.getContentType() != null && contentTypeHeader == null) { + partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); + } + return new StandardOperationRequestPart(part.getName(), StringUtils.hasText(part + .getSubmittedFileName()) ? part.getSubmittedFileName() : null, + FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); + } + + private List extractMultipartRequestParts( + MockMultipartHttpServletRequest multipartRequest) throws IOException { + List parts = new ArrayList<>(); + for (Entry> entry : multipartRequest + .getMultiFileMap().entrySet()) { + for (MultipartFile file : entry.getValue()) { + parts.add(createOperationRequestPart(file)); } } return parts; } + private StandardOperationRequestPart createOperationRequestPart(MultipartFile file) + throws IOException { + HttpHeaders partHeaders = new HttpHeaders(); + if (StringUtils.hasText(file.getContentType())) { + partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); + } + return new StandardOperationRequestPart(file.getName(), StringUtils.hasText(file + .getOriginalFilename()) ? file.getOriginalFilename() : null, + file.getBytes(), partHeaders); + } + private HttpHeaders extractHeaders(Part part) { HttpHeaders partHeaders = new HttpHeaders(); for (String headerName : part.getHeaderNames()) { From 36dfaecffea76d086b285a4232898dd5edf3497e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 12:54:30 +0100 Subject: [PATCH 0131/1059] Improve unit testing of StandardTemplateResourceResolver --- ...StandardTemplateResourceResolverTests.java | 126 ++++++++++++++++++ .../restdocs/templates/test.snippet | 0 2 files changed, 126 insertions(+) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java new file mode 100644 index 000000000..bb221c764 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -0,0 +1,126 @@ +/* + * 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.templates; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.Resource; + +/** + * Tests for {@link TemplateResourceResolver}. + * + * @author Andy Wilkinson + */ +public class StandardTemplateResourceResolverTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver(); + + private final TestClassLoader classLoader = new TestClassLoader(); + + @Test + public void customSnippetResolution() throws Exception { + this.classLoader.addResource( + "org/springframework/restdocs/templates/test.snippet", getClass() + .getResource("test.snippet")); + Resource snippet = doWithThreadContextClassLoader(this.classLoader, + new Callable() { + + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver + .resolveTemplateResource("test"); + } + + }); + + assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + } + + @Test + public void fallsBackToDefaultSnippet() throws Exception { + this.classLoader.addResource( + "org/springframework/restdocs/templates/default-test.snippet", getClass() + .getResource("test.snippet")); + Resource snippet = doWithThreadContextClassLoader(this.classLoader, + new Callable() { + + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver + .resolveTemplateResource("test"); + } + + }); + + assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + } + + @Test + public void failsIfCustomAndDefaultSnippetDoNotExist() throws Exception { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage(equalTo("Template named 'test' could not be resolved")); + doWithThreadContextClassLoader(this.classLoader, new Callable() { + + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver + .resolveTemplateResource("test"); + } + + }); + } + + private T doWithThreadContextClassLoader(ClassLoader classLoader, + Callable action) throws Exception { + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + try { + return action.call(); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + } + + private static class TestClassLoader extends ClassLoader { + + private Map resources = new HashMap<>(); + + private void addResource(String name, URL url) { + this.resources.put(name, url); + } + + @Override + public URL getResource(String name) { + return this.resources.get(name); + } + + } + +} diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet new file mode 100644 index 000000000..e69de29bb From cbbdbcb201b0dc467b194fce89d5d88f45bf4cb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 13:51:13 +0100 Subject: [PATCH 0132/1059] =?UTF-8?q?Allow=20subclasses=20to=20access=20a?= =?UTF-8?q?=20snippet=E2=80=99s=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates each of the built-in snippet implementations to provide protected getter methods for their fields. This allows subclasses to access and work with the snippet’s state. See gh-73 --- .../restdocs/hypermedia/LinksSnippet.java | 25 ++++++++++++++----- .../payload/AbstractFieldsSnippet.java | 10 ++++++++ .../request/AbstractParametersSnippet.java | 11 ++++++++ .../restdocs/snippet/TemplatedSnippet.java | 19 ++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index a182d8df6..edacab162 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -47,8 +47,6 @@ public class LinksSnippet extends TemplatedSnippet { private final Map descriptorsByRel = new LinkedHashMap<>(); - private final Set requiredRels = new HashSet<>(); - private final LinkExtractor linkExtractor; /** @@ -79,9 +77,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip Assert.hasText(descriptor.getRel()); Assert.hasText(descriptor.getDescription()); this.descriptorsByRel.put(descriptor.getRel(), descriptor); - if (!descriptor.isOptional()) { - this.requiredRels.add(descriptor.getRel()); - } } } @@ -105,7 +100,15 @@ private void validate(Map> links) { Set undocumentedRels = new HashSet(actualRels); undocumentedRels.removeAll(this.descriptorsByRel.keySet()); - Set missingRels = new HashSet(this.requiredRels); + Set requiredRels = new HashSet<>(); + for (Entry relAndDescriptor : this.descriptorsByRel + .entrySet()) { + if (!relAndDescriptor.getValue().isOptional()) { + requiredRels.add(relAndDescriptor.getKey()); + } + } + + Set missingRels = new HashSet(requiredRels); missingRels.removeAll(actualRels); if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { @@ -133,4 +136,14 @@ private List> createLinksModel() { return model; } + /** + * Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their + * {@link LinkDescriptor#getRel() rels}. + * + * @return the link descriptors + */ + protected final Map getDescriptorsByRel() { + return this.descriptorsByRel; + } + } \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 33aa114cb..80cd37080 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -146,4 +146,14 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { */ protected abstract byte[] getContent(Operation operation) throws IOException; + /** + * Returns the list of {@link FieldDescriptor FieldDescriptors} that will be used to + * generate the documentation. + * + * @return the field descriptors + */ + protected final List getFieldDescriptors() { + return this.fieldDescriptors; + } + } \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index bafbdd37e..804fb591b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -94,4 +94,15 @@ protected void verifyParameterDescriptors(Operation operation) { protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); + /** + * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will + * be used to generate the documentation key by their + * {@link ParameterDescriptor#getName()}. + * + * @return the map of path descriptors + */ + protected final Map getFieldDescriptors() { + return this.descriptorsByName; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 1e7dcc1df..3ec4afbb4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -81,4 +81,23 @@ public void document(Operation operation) throws IOException { */ protected abstract Map createModel(Operation operation); + /** + * Returns the additional attributes that will be included in the model during + * template rendering. + * + * @return the additional attributes + */ + protected final Map getAttributes() { + return this.attributes; + } + + /** + * Returns the name of the snippet that will be created. + * + * @return the snippet name + */ + protected final String getSnippetName() { + return this.snippetName; + } + } \ No newline at end of file From 94d5e2d94f2dc6a509f3da5e5dd71e1870b53ae1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 14:49:51 +0100 Subject: [PATCH 0133/1059] Improve the extensibility of the descriptor classes This commit improves the extensibility of the various descriptor classes. The following changes have been made: - Constructors have been made protected to allow them to be called by subclasses in a different package - Model creation has been moved to the relevant snippet class. This improves the separation of concerns as a descriptor should not be aware of template-based rendering and the need for a model. The method is protected to allow it to be overridden by a custom subclass that uses a custom descriptor subclass. - Methods that should not be overridden have been made final. See gh-73 --- .../restdocs/hypermedia/LinkDescriptor.java | 43 +++++++++------ .../restdocs/hypermedia/LinksSnippet.java | 17 +++++- .../payload/AbstractFieldsSnippet.java | 18 ++++++- .../restdocs/payload/FieldDescriptor.java | 53 ++++++++++++------- .../request/AbstractParametersSnippet.java | 16 +++++- .../restdocs/request/ParameterDescriptor.java | 35 ++++++------ .../restdocs/snippet/AbstractDescriptor.java | 4 +- 7 files changed, 129 insertions(+), 57 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 06689592d..9d11f9156 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.hypermedia; -import java.util.HashMap; -import java.util.Map; - import org.springframework.restdocs.snippet.AbstractDescriptor; /** @@ -36,7 +33,12 @@ public class LinkDescriptor extends AbstractDescriptor { private boolean optional; - LinkDescriptor(String rel) { + /** + * Creates a new {@code LinkDescriptor} describing a link with the given {@code rel}. + * + * @param rel the rel of the link + */ + protected LinkDescriptor(String rel) { this.rel = rel; } @@ -46,7 +48,7 @@ public class LinkDescriptor extends AbstractDescriptor { * @param description The link's description * @return {@code this} */ - public LinkDescriptor description(String description) { + public final LinkDescriptor description(String description) { this.description = description; return this; } @@ -56,29 +58,36 @@ public LinkDescriptor description(String description) { * * @return {@code this} */ - public LinkDescriptor optional() { + public final LinkDescriptor optional() { this.optional = true; return this; } - String getRel() { + /** + * Returns the rel of the link described by this descriptor + * + * @return the rel + */ + public final String getRel() { return this.rel; } - String getDescription() { + /** + * Returns the description for the link + * + * @return the link description + */ + public final String getDescription() { return this.description; } - boolean isOptional() { + /** + * Returns {@code true} if the described link is optional, otherwise {@code false} + * + * @return {@code true} if the described link is optional, otherwise {@code false} + */ + public final boolean isOptional() { return this.optional; } - Map toModel() { - Map model = new HashMap<>(); - model.put("rel", this.rel); - model.put("description", this.description); - model.put("optional", this.optional); - model.putAll(getAttributes()); - return model; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index edacab162..006327bb0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -131,7 +131,7 @@ private void validate(Map> links) { private List> createLinksModel() { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { - model.add(entry.getValue().toModel()); + model.add(createModelForDescriptor(entry.getValue())); } return model; } @@ -146,4 +146,19 @@ protected final Map getDescriptorsByRel() { return this.descriptorsByRel; } + /** + * Returns a model for the given {@code descriptor} + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(LinkDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("rel", descriptor.getRel()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + } \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 80cd37080..970cd7ec1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -77,7 +77,7 @@ protected Map createModel(Operation operation) { List> fields = new ArrayList<>(); model.put("fields", fields); for (FieldDescriptor descriptor : this.fieldDescriptors) { - fields.add(descriptor.toModel()); + fields.add(createModelForDescriptor(descriptor)); } return model; } @@ -156,4 +156,20 @@ protected final List getFieldDescriptors() { return this.fieldDescriptors; } + /** + * Returns a model for the given {@code descriptor} + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(FieldDescriptor descriptor) { + Map model = new HashMap(); + model.put("path", descriptor.getPath()); + model.put("type", descriptor.getType().toString()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + } \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 5cbf63843..b9cac2dfa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.payload; -import java.util.HashMap; -import java.util.Map; - import org.springframework.restdocs.snippet.AbstractDescriptor; /** @@ -39,7 +36,12 @@ public class FieldDescriptor extends AbstractDescriptor { private String description; - FieldDescriptor(String path) { + /** + * Creates a new {@code FieldDescriptor} describing the field with the given + * {@code path}. + * @param path the path + */ + protected FieldDescriptor(String path) { this.path = path; } @@ -51,7 +53,7 @@ public class FieldDescriptor extends AbstractDescriptor { * @return {@code this} * @see JsonFieldType */ - public FieldDescriptor type(Object type) { + public final FieldDescriptor type(Object type) { this.type = type; return this; } @@ -61,7 +63,7 @@ public FieldDescriptor type(Object type) { * * @return {@code this} */ - public FieldDescriptor optional() { + public final FieldDescriptor optional() { this.optional = true; return this; } @@ -72,34 +74,45 @@ public FieldDescriptor optional() { * @param description The field's description * @return {@code this} */ - public FieldDescriptor description(String description) { + public final FieldDescriptor description(String description) { this.description = description; return this; } - String getPath() { + /** + * Returns the path of the field described by this descriptor + * + * @return the path + */ + public final String getPath() { return this.path; } - Object getType() { + /** + * Returns the type of the field described by this descriptor + * + * @return the type + */ + public final Object getType() { return this.type; } - boolean isOptional() { + /** + * Returns {@code true} if the described field is optional, otherwise {@code false} + * + * @return {@code true} if the described field is optional, otherwise {@code false} + */ + public final boolean isOptional() { return this.optional; } - String getDescription() { + /** + * Returns the description for the field + * + * @return the field description + */ + public final String getDescription() { return this.description; } - Map toModel() { - Map model = new HashMap(); - model.put("path", this.path); - model.put("type", this.type.toString()); - model.put("description", this.description); - model.put("optional", this.optional); - model.putAll(this.getAttributes()); - return model; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 804fb591b..be6d1ee87 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -67,7 +67,7 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); for (Entry entry : this.descriptorsByName.entrySet()) { - parameters.add(entry.getValue().toModel()); + parameters.add(createModelForDescriptor(entry.getValue())); } model.put("parameters", parameters); return model; @@ -105,4 +105,18 @@ protected final Map getFieldDescriptors() { return this.descriptorsByName; } + /** + * Returns a model for the given {@link descriptor}. + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(ParameterDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.putAll(descriptor.getAttributes()); + return model; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 8edaf3ccf..549500daa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.request; -import java.util.HashMap; -import java.util.Map; - import org.springframework.restdocs.snippet.AbstractDescriptor; /** @@ -34,7 +31,13 @@ public class ParameterDescriptor extends AbstractDescriptor private String description; - ParameterDescriptor(String name) { + /** + * Creates a new {@code ParameterDescriptor} describing the parameter with the given + * {@code name}. + * + * @param name the name of the parameter + */ + protected ParameterDescriptor(String name) { this.name = name; } @@ -44,25 +47,27 @@ public class ParameterDescriptor extends AbstractDescriptor * @param description The parameter's description * @return {@code this} */ - public ParameterDescriptor description(String description) { + public final ParameterDescriptor description(String description) { this.description = description; return this; } - String getName() { + /** + * Returns the name of the parameter being described by this descriptor + * + * @return the name of the parameter + */ + public final String getName() { return this.name; } - String getDescription() { + /** + * Returns the description of the parameter + * + * @return the description + */ + public final String getDescription() { return this.description; } - Map toModel() { - Map model = new HashMap<>(); - model.put("name", this.name); - model.put("description", this.description); - model.putAll(getAttributes()); - return model; - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index 4766a171e..931cd79ed 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -34,7 +34,7 @@ public abstract class AbstractDescriptor> { private Map attributes = new HashMap<>(); /** - * Sets the descriptor's attributes + * Adds the given {@code attributes} to the descriptor * * @param attributes the attributes * @return the descriptor @@ -52,7 +52,7 @@ public T attributes(Attribute... attributes) { * * @return the attributes */ - protected Map getAttributes() { + public final Map getAttributes() { return this.attributes; } From 63e74663042d3e9c27e3045c140399407ea26312 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 15:01:32 +0100 Subject: [PATCH 0134/1059] Fix javadoc in AbstractParametersSnippet --- .../restdocs/request/AbstractParametersSnippet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index be6d1ee87..fcb8ae2b7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -106,7 +106,7 @@ protected final Map getFieldDescriptors() { } /** - * Returns a model for the given {@link descriptor}. + * Returns a model for the given {@code descriptor}. * * @param descriptor the descriptor * @return the model From fe84aef912657742ed8cb7eecaab0e03e8d021cf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 15:48:11 +0100 Subject: [PATCH 0135/1059] Polish the documentation --- docs/src/docs/asciidoc/configuration.adoc | 2 +- .../customizing-requests-and-responses.adoc | 26 ++++++++++------ .../docs/asciidoc/documenting-your-api.adoc | 31 ++++++++++--------- docs/src/docs/asciidoc/getting-started.adoc | 28 ++++++++--------- 4 files changed, 47 insertions(+), 40 deletions(-) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index acd0fd2f4..0dd0779bb 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -38,7 +38,7 @@ TIP: To configure a request's context path, use the `contextPath` method on === Snippet encoding The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same -default for the snippets that it generated. If you require an encoding other than `UTF-8`, +default for the snippets that it generates. If you require an encoding other than `UTF-8`, use `RestDocumentationConfigurer` to configure it: [source,java,indent=0] diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 64830cd9f..f88a58795 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -7,7 +7,8 @@ preprocessors that can be used to modify a request or response before it's docum Preprocessing is configured by calling `document` with an `OperationRequestPreprocessor`, and/or an `OperationResponsePreprocessor`. Instances can be obtained using the -static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`: +static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`. For +example: [source,java,indent=0] ---- @@ -42,20 +43,25 @@ include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=use] use of `alwaysDo` above. Various built in preprocessors, including those illustrated above, are available via the -static methods on `Preprocessors`. See below for further details. +static methods on `Preprocessors`. See <> for further details. -[[customizing-requests-and-responses-pretty-printing]] -=== Pretty printing +[[customizing-requests-and-responses-preprocessors]] +=== Preprocessors + + + +[[customizing-requests-and-responses-preprocessors-pretty-print]] +==== Pretty printing `prettyPrint` on `Preprocessors` formats the content of the request or response to make it easier to read. -[[customizing-requests-and-responses-masking-links]] -=== Masking links +[[customizing-requests-and-responses-preprocessors-mask-links]] +==== Masking links If you're documenting a Hypermedia-based API, you may want to encourage clients to navigate the API using links rather than through the use of hard coded URIs. One way to do @@ -65,16 +71,16 @@ different replacement can also be specified if you wish. -[[customizing-requests-and-responses-removing-headers]] -=== Removing headers +[[customizing-requests-and-responses-preprocessors-remove-headers]] +==== Removing headers `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. -[[customizing-requests-and-responses-replacing-patterns]] -=== Replacing patterns +[[customizing-requests-and-responses-preprocessors-replace-patterns]] +==== Replacing patterns `replacePattern` on `Preprocessors` provides a general purpose mechanism for replacing content in a request or response. Any occurrences of a regular expression are diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c4352df42..e54513838 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -57,9 +57,9 @@ response. [[documenting-your-api-request-response-payloads]] === Request and response payloads -In addition to the Hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided: +provided. For example: [source,java,indent=0] ---- @@ -204,26 +204,27 @@ method will be included in the documentation. === Request parameters A request's parameters can be documented using `requestParameters`. Request parameters -can be included in a `GET` requests query string: +can be included in a `GET` request's query string. For example: [source,java,indent=0] ---- include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-query-string] ---- -<1> Perform a `GET` request with two parameters in the query string. +<1> Perform a `GET` request with two parameters, `page` and `per_page` in the query + string. <2> Produce a snippet describing the request's parameters. Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document a parameter named `page`. Uses the static `parameterWithName` method on +<3> Document the `page` parameter. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. -<4> Document a parameter named `per_page`. +<4> Document the `per_page` parameter. -They can also be included as form data in the body of a POST request: +Request parameters can also be included as form data in the body of a POST request: [source,java,indent=0] ---- include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-form-data] ---- -<1> Perform a `POST` request with a single parameter. +<1> Perform a `POST` request with a single parameter, `username`. In both cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. @@ -237,18 +238,18 @@ request parameter is not found in the request. [[documenting-your-api-path-parameters]] === Path parameters -A request's path parameters can be documented using `pathParameters` +A request's path parameters can be documented using `pathParameters`. For example: [source,java,indent=0] ---- include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] ---- -<1> Build the request. Uses the static `get` method on `RestDocumentationRequestBuilders`. +<1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. <2> Produce a snippet describing the request's path parameters. Uses the static `pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document a parameter named `longitude`. Uses the static `parameterWithName` method on +<3> Document the parameter named `latitude`. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. -<4> Document a parameter named `latitude`. +<4> Document the parameter named `longitude`. The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. @@ -268,7 +269,7 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than Spring REST Docs provides a number of classes that can help you to document constraints. An instance of `ConstraintDescriptions` can be used to access descriptions of a class's -constraints: +constraints. For example: [source,java,indent=0] ---- @@ -400,8 +401,8 @@ are supported: For example, `document("{method-name}")` in a test method named `creatingANote` will write snippets into a directory named `creating-a-note`. -The `{step}` parameter is particularly useful in combination with Spring MVC Test's -`alwaysDo` functionality. It allows documentation to be configured once in a setup method: +A parameterized output directory is particularly useful in combination with Spring MVC +Test's `alwaysDo` functionality. It allows documentation to be configured once in a setup method: [source,java,indent=0] ---- diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index c013ce47d..8273b296b 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -13,8 +13,7 @@ If you want to jump straight in, there are two sample applications available. http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and {samples}/rest-notes-spring-data-rest[the other] uses http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use -Spring REST Docs to produce a detailed API guide and a getting started walkthrough. You -can use either Gradle or Maven to build them. +Spring REST Docs to produce a detailed API guide and a getting started walkthrough. In each sample the source for the documentation can be found in `src/main/asciidoc`. `api-guide.adoc` produces an API guide for the service. `getting-started-guide.adoc` @@ -36,7 +35,7 @@ The first step in using Spring REST Docs is to configure your project's build. [[getting-started-build-configuration-gradle]] ==== Gradle build configuration -The {samples/rest-notes-spring-hateoas}[Spring HATEOAS sample] contains a `build.gradle` +The {samples}rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -65,7 +64,7 @@ described below. } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on spring-restdocs in the `testCompile` configuration. +<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. <3> Configure a property to define the output location for generated snippets. <4> Configure the `test` task to add the snippets directory as an output. <5> Configure the `asciidoctor` task @@ -100,7 +99,7 @@ directory: [[getting-started-build-configuration-maven]] ==== Maven build configuration -The {samples/rest-notes-spring-data-rest}[Spring Data REST sample] contains a `pom.xml` +The {samples}rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -135,7 +134,7 @@ described below. generate-docs - package <5> + package <6> process-asciidoc @@ -143,7 +142,7 @@ described below. html book - ${snippetsDirectory} + ${snippetsDirectory} <5> @@ -157,9 +156,10 @@ described below. <2> Configure a property to define the output location for generated snippets. <3> Add the SureFire plugin and configure it to include files whose names end with `Documentation.java`. -<4> Add the Asciidoctor plugin and configure it to define an attribute named `snippets` - that can be used when including the generated snippets in your documentation. -<5> [[getting-started-build-configuration-maven-plugin-phase]] If you want to +<4> Add the Asciidoctor plugin +<5> Define an attribute named `snippets` that can be used when including the generated + snippets in your documentation. +<6> [[getting-started-build-configuration-maven-plugin-phase]] If you want to <> in your project's jar you should use the `prepare-package` phase. @@ -226,8 +226,8 @@ documentation snippets for the result's request and response. ==== Setting up Spring MVC test The first step in generating documentation snippets is to declare a `public` -`RestDocumentation` that's annotated as a JUnit `@Rule` and to provide an `@Before` method -that creates a `MockMvc` instance: +`RestDocumentation` field that's annotated as a JUnit `@Rule` and to provide an `@Before` +method that creates a `MockMvc` instance: [source,java,indent=0] ---- @@ -250,7 +250,7 @@ sensible defaults and also provides an API for customizing the configuration. Re ==== Invoking the RESTful service Now that a `MockMvc` instance has been created, it can be used to invoke the RESTful -service and document the request and response. +service and document the request and response. For example: [source,java,indent=0] ---- @@ -282,7 +282,7 @@ that can be produced by Spring REST Docs. The generated snippets can be included in your documentation using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. The `snippets` attribute specified in the <> can be used to reference the snippets output directory, for example: +configuration>> can be used to reference the snippets output directory. For example: [source,adoc,indent=0] ---- From 1e5350295871e51d593f334441ef893adefae919 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Sep 2015 16:07:15 +0100 Subject: [PATCH 0136/1059] Make milestone builds are available to the samples --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index d478c005c..ad8688f23 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -118,7 +118,7 @@ spring-snapshots Spring snapshots - https://repo.spring.io/snapshot + https://repo.spring.io/libs-snapshot true diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 7ab7a4045..a099b066e 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'eclipse' repositories { mavenLocal() - maven { url 'https://repo.spring.io/snapshot' } + maven { url 'https://repo.spring.io/libs-snapshot' } mavenCentral() } From 5dc28b9bb010ba61c03401a9866328748bbc5b95 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Sep 2015 10:08:28 +0100 Subject: [PATCH 0137/1059] Improve extensibility of RestDocumentationContextPlaceholderResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, RestDocumentationContextPlaceholderResolver could be subclassed, but it didn’t provide access to its state (the RestDocumentationContext). This commit opens up RestDocumentationContextPlaceholderResolver by adding a getter method for its RestDocumentationContext. It also makes the kebab-base and snake_case conversion methods protected (rather than private) so that they can be used by subclasses. See gh-116 --- .../RestDocumentationContextPlaceholderResolver.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 706078d05..8a2f93142 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -66,22 +66,26 @@ public String resolvePlaceholder(String placeholderName) { return this.context.getTestMethodName(); } if ("method-name".equals(placeholderName)) { - return camelCaseToDash(this.context.getTestMethodName()); + return camelCaseToKebabCase(this.context.getTestMethodName()); } if ("method_name".equals(placeholderName)) { - return camelCaseToUnderscore(this.context.getTestMethodName()); + return camelCaseToSnakeCase(this.context.getTestMethodName()); } return null; } - private String camelCaseToDash(String string) { + protected final String camelCaseToKebabCase(String string) { return camelCaseToSeparator(string, "-"); } - private String camelCaseToUnderscore(String string) { + protected final String camelCaseToSnakeCase(String string) { return camelCaseToSeparator(string, "_"); } + protected final RestDocumentationContext getContext() { + return this.context; + } + private String camelCaseToSeparator(String string, String separator) { Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); StringBuffer result = new StringBuffer(); From 6dcfdf72065258d0c7b952f708a7ccf00107d7b5 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Wed, 9 Sep 2015 12:05:26 +0200 Subject: [PATCH 0138/1059] Update getting started documentation to use new class names Closes gh-118 --- docs/src/docs/asciidoc/getting-started.adoc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8273b296b..ed25c8abf 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -238,10 +238,11 @@ The `RestDocumentation` rule is configured with the output directory into which generated snippets should be written. This output directory should match the snippets directory that you have configured in your `build.gradle` or `pom.xml` file. -The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An instance -of this class can be obtained from the static `documentationConfiguration()` method on -`org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies -sensible defaults and also provides an API for customizing the configuration. Refer to the +The `MockMvc` instance is configured using a `RestDocumentationMockMvcConfigurer`. An +instance of this class can be obtained from the static `documentationConfiguration()` +method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. +`RestDocumentationMockMvcConfigurer` applies sensible defaults and also provides an API +for customizing the configuration. Refer to the <> for more information. From ed3d5f0a5971c8f554b1cb6cbb8cab87cb95163a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Sep 2015 11:47:19 +0100 Subject: [PATCH 0139/1059] Add javadoc to protected methods added in 5dc28b9 --- ...ocumentationContextPlaceholderResolver.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 8a2f93142..a517598d4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -74,14 +74,32 @@ public String resolvePlaceholder(String placeholderName) { return null; } + /** + * Converts the given {@code string} from camelCase to kebab-case. + * + * @param string the string + * @return the converted string + */ protected final String camelCaseToKebabCase(String string) { return camelCaseToSeparator(string, "-"); } + /** + * Converts the given {@code string} from camelCase to snake_case. + * + * @param string the string + * @return the converted string + */ protected final String camelCaseToSnakeCase(String string) { return camelCaseToSeparator(string, "_"); } + /** + * Returns the {@link RestDocumentationContext} that should be used during placeholder + * resolution. + * + * @return the context + */ protected final RestDocumentationContext getContext() { return this.context; } From 11c0945d10f6b050702ff8893a940a0bd05df04e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 10 Sep 2015 10:00:18 +0100 Subject: [PATCH 0140/1059] Polish the documentation produced by the samples - Correct the location of the Git repository - Correct the location of the sample within the repository - Update the build instructions for the Data REST sample to use Maven - Move the table of contents to the left - Add section links Closes gh-113 --- .../src/main/asciidoc/api-guide.adoc | 8 ++++---- .../main/asciidoc/getting-started-guide.adoc | 20 +++++++++---------- .../src/main/asciidoc/api-guide.adoc | 8 ++++---- .../main/asciidoc/getting-started-guide.adoc | 14 ++++++------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 58cb3ad5b..002b8a441 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -1,11 +1,11 @@ = RESTful Notes API Guide Andy Wilkinson; :doctype: book -:toc: -:sectanchors: -:sectlinks: -:toclevels: 4 +:icons: font :source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: [[overview]] = Overview diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc index a6678bb55..420681011 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc @@ -1,9 +1,11 @@ -= RESTful Notes User Guide += RESTful Notes Getting Started Guide Andy Wilkinson; :doctype: book -:toc: -:toclevels: 4 +:icons: font :source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: [introduction] = Introduction @@ -21,22 +23,20 @@ to describe the relationships between resources and to allow navigation between RESTful Notes is written using http://projects.spring.io/spring-boot[Spring Boot] which makes it easy to get it up and running so that you can start exploring the REST API. -At the moment, binaries for RESTful Notes are not published anywhere. However, building -and running it from source is straightforward. The first step is to clone the Git -repository: +The first step is to clone the Git repository: [source,bash] ---- -$ git clone https://github.com/wilkinsona/spring-restdocs +$ git clone https://github.com/spring-projects/spring-restdocs ---- Once the clone is complete, you're ready to get the service up and running: [source,bash] ---- -$ cd rest-notes -$ ./gradlew build -$ java -jar build/libs/*.jar +$ cd samples/rest-notes-spring-data-rest +$ mvn clean package +$ java -jar target/*.jar ---- You can check that the service is up and running by executing a simple request using diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index bf6c25ea8..f0ca947eb 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -1,11 +1,11 @@ = RESTful Notes API Guide Andy Wilkinson; :doctype: book -:toc: -:sectanchors: -:sectlinks: -:toclevels: 4 +:icons: font :source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: [[overview]] = Overview diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc index 676a4615c..096e00abe 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc @@ -1,9 +1,11 @@ = RESTful Notes Getting Started Guide Andy Wilkinson; :doctype: book -:toc: -:toclevels: 4 +:icons: font :source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: [introduction] = Introduction @@ -21,20 +23,18 @@ to describe the relationships between resources and to allow navigation between RESTful Notes is written using http://projects.spring.io/spring-boot[Spring Boot] which makes it easy to get it up and running so that you can start exploring the REST API. -At the moment, binaries for RESTful Notes are not published anywhere. However, building -and running it from source is straightforward. The first step is to clone the Git -repository: +The first step is to clone the Git repository: [source,bash] ---- -$ git clone https://github.com/wilkinsona/spring-restdocs +$ git clone https://github.com/spring-projects/spring-restdocs ---- Once the clone is complete, you're ready to get the service up and running: [source,bash] ---- -$ cd rest-notes +$ cd samples/rest-notes-spring-hateoas $ ./gradlew build $ java -jar build/libs/*.jar ---- From dc9af9599e27e4df27ec7cc39ca08703ecb695bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 10 Sep 2015 10:10:05 +0100 Subject: [PATCH 0141/1059] Include documentation samples in the main docs zip Previously, to see an example of the documentation that can be produced by Spring REST Docs, users had to clone the repository and build one of the samples. This commit makes it easier to see what Spring REST Docs can do by updating the docs zip that's automatically published to docs.spring.io to include both the API Guide and the Getting Started Guide from the RESTful notes sample. Closes gh-112 --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 338df543a..b593dfb90 100644 --- a/build.gradle +++ b/build.gradle @@ -153,7 +153,7 @@ task api (type: Javadoc) { } } -task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api']) { +task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api', ':buildSamples']) { group = 'Distribution' baseName = 'spring-restdocs' classifier = 'docs' @@ -167,6 +167,10 @@ task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api']) { from(api) { into 'api' } + + from(file('samples/rest-notes-spring-hateoas/build/asciidoc/html5')) { + into 'samples/restful-notes' + } } configurations { From ebab967e439165d614948d3ce7a132a50cf4713c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 13 Sep 2015 16:02:19 +0100 Subject: [PATCH 0142/1059] Make ContentModifier part of the public API Previously, ContentModifier and ContentModifyingOperationPreprocessor were package-private. This meant that anyone wishing to perform content modification during preprocessing had to reimplement much of the existing content modifying preprocessor, that than just having to implement ContentModifier. This commits make both ContentModifier and ContentModifyingOperationPreprocessor public so that all custom content modification is now a matter of implementing a custom ContentModifier and creating an instance of ContentModifyingOperationPreprocessor. Closes gh-121 --- .../restdocs/operation/preprocess/ContentModifier.java | 2 +- .../ContentModifyingOperationPreprocessor.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java index 52e1075df..54fb82795 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java @@ -27,7 +27,7 @@ * @author Andy Wilkinson * @see ContentModifyingOperationPreprocessor */ -interface ContentModifier { +public interface ContentModifier { /** * Returns modified content based on the given {@code originalContent} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 1ff1e8191..0531c46d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -28,11 +28,17 @@ * * @author Andy Wilkinson */ -class ContentModifyingOperationPreprocessor implements OperationPreprocessor { +public class ContentModifyingOperationPreprocessor implements OperationPreprocessor { private final ContentModifier contentModifier; - ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { + /** + * Create a new {@code ContentModifyingOperationPreprocessor} that will apply the + * given {@code contentModifier} to the operation's request or response. + * + * @param contentModifier the contentModifier + */ + public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { this.contentModifier = contentModifier; } From df40f63238f729d104e8c348bae65472a0a754f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 13 Sep 2015 16:39:54 +0100 Subject: [PATCH 0143/1059] Add support for using the class name in parameterised operation name This commit adds support for three new placeholders that can be used when specifying the name of an operation: {ClassName}, {class-name}, and {class_name}. This allows the operation name to be configured once in an abstract superclass and reused across multiple test classes. Closes gh-116 --- .../docs/asciidoc/documenting-your-api.adoc | 19 +++++++++--- ...cumentationContextPlaceholderResolver.java | 27 ++++++++++++++--- ...tationContextPlaceholderResolverTests.java | 29 +++++++++++++++++-- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index e54513838..1bbc2b05a 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -386,7 +386,7 @@ are supported: | Parameter | Description | {methodName} -| The name of the test method, formatted using camelCase +| The unmodified name of the test method | {method-name} | The name of the test method, formatted using kebab-case @@ -394,15 +394,26 @@ are supported: | {method_name} | The name of the test method, formatted using snake_case +| {ClassName} +| The unmodified name of the test class + +| {class-name} +| The name of the test class, formatted using kebab-case + +| {class_name} +| The nome of the test class, formatted using snake_case + | {step} | The count of calls to MockMvc.perform in the current test |=== -For example, `document("{method-name}")` in a test method named `creatingANote` will write -snippets into a directory named `creating-a-note`. +For example, `document("{class-name}/{method-name}")` in a test method named +`creatingANote` on the test class `GettingStartedDocumentaiton`, will write +snippets into a directory named `getting-started-documentation/creating-a-note`. A parameterized output directory is particularly useful in combination with Spring MVC -Test's `alwaysDo` functionality. It allows documentation to be configured once in a setup method: +Test's `alwaysDo` functionality. It allows documentation to be configured once in a setup +method: [source,java,indent=0] ---- diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index a517598d4..51f63c7b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -28,15 +28,23 @@ *

      *
    • {@code step} – the {@link RestDocumentationContext#getStepCount() step current * count}. - *
    • {@code methodName} - the name of the - * {@link RestDocumentationContext#getTestMethodName() current test method} formatted - * using camelCase + *
    • {@code methodName} - the unmodified name of the + * {@link RestDocumentationContext#getTestMethodName() current test method} without + * applying any formatting *
    • {@code method-name} - the name of the * {@link RestDocumentationContext#getTestMethodName() current test method} formatted * using kebab-case *
    • {@code method_name} - the name of the * {@link RestDocumentationContext#getTestMethodName() current test method} formatted * using snake_case + *
    • {@code ClassName} - the unmodified {@link Class#getSimpleName() simple name} of the + * {@link RestDocumentationContext#getTestClass() current test class} + *
    • {@code class-name} - the {@link Class#getSimpleName() simple name} of the + * {@link RestDocumentationContext#getTestClass() current test class} formatted using + * kebab-case + *
    • {@code class_name} - the {@link Class#getSimpleName() simple name} of the + * {@link RestDocumentationContext#getTestClass() current test class} formatted using + * snake case *
    * * @author Andy Wilkinson @@ -71,6 +79,15 @@ public String resolvePlaceholder(String placeholderName) { if ("method_name".equals(placeholderName)) { return camelCaseToSnakeCase(this.context.getTestMethodName()); } + if ("ClassName".equals(placeholderName)) { + return this.context.getTestClass().getSimpleName(); + } + if ("class-name".equals(placeholderName)) { + return camelCaseToKebabCase(this.context.getTestClass().getSimpleName()); + } + if ("class_name".equals(placeholderName)) { + return camelCaseToSnakeCase(this.context.getTestClass().getSimpleName()); + } return null; } @@ -108,7 +125,9 @@ private String camelCaseToSeparator(String string, String separator) { Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); StringBuffer result = new StringBuffer(); while (matcher.find()) { - matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase()); + String replacement = (matcher.start() > 0) ? separator + + matcher.group(1).toLowerCase() : matcher.group(1).toLowerCase(); + matcher.appendReplacement(result, replacement); } matcher.appendTail(result); return result.toString(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index ff06d8b3a..37c19160a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -32,14 +32,14 @@ public class RestDocumentationContextPlaceholderResolverTests { @Test - public void dashSeparatedMethodName() throws Exception { + public void kebabCaseMethodName() throws Exception { assertThat( createResolver("dashSeparatedMethodName").resolvePlaceholder( "method-name"), equalTo("dash-separated-method-name")); } @Test - public void underscoreSeparatedMethodName() throws Exception { + public void snakeCaseMethodName() throws Exception { assertThat( createResolver("underscoreSeparatedMethodName").resolvePlaceholder( "method_name"), equalTo("underscore_separated_method_name")); @@ -52,14 +52,37 @@ public void camelCaseMethodName() throws Exception { equalTo("camelCaseMethodName")); } + @Test + public void kebabCaseClassName() throws Exception { + assertThat(createResolver().resolvePlaceholder("class-name"), + equalTo("rest-documentation-context-placeholder-resolver-tests")); + } + + @Test + public void snakeCaseClassName() throws Exception { + assertThat(createResolver().resolvePlaceholder("class_name"), + equalTo("rest_documentation_context_placeholder_resolver_tests")); + } + + @Test + public void camelCaseClassName() throws Exception { + assertThat(createResolver().resolvePlaceholder("ClassName"), + equalTo("RestDocumentationContextPlaceholderResolverTests")); + } + @Test public void stepCount() throws Exception { assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("0")); } + private PlaceholderResolver createResolver() { + return new RestDocumentationContextPlaceholderResolver( + new RestDocumentationContext(getClass(), null, null)); + } + private PlaceholderResolver createResolver(String methodName) { return new RestDocumentationContextPlaceholderResolver( - new RestDocumentationContext(null, methodName, null)); + new RestDocumentationContext(getClass(), methodName, null)); } } From 14ad59d434dd0be4a1202847f51c7f966559de10 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 13 Sep 2015 11:58:22 -0400 Subject: [PATCH 0144/1059] Make descriptions more flexible Previously, the description of a link, parameter, or field had to be a String. Given the use of Mustache templates, this was unnecessarily restrictive. It prevented the use of a richer object, the components of which could then be accessed in a template. This commit changes the description of links, parameters, and fields to be an Object, thereby allowing richer objects to be used. The default template will call toString on the description during rendering. Custom templates can be used to make more sophisticated use of the description. To ensure that the description is consistent across all descriptor types, it has been moved up onto the AbstractDescriptor superclass. Closes gh-123 --- .../restdocs/hypermedia/LinkDescriptor.java | 22 ---------------- .../restdocs/hypermedia/LinksSnippet.java | 2 +- .../payload/AbstractFieldsSnippet.java | 2 +- .../restdocs/payload/FieldDescriptor.java | 22 ---------------- .../request/AbstractParametersSnippet.java | 2 +- .../restdocs/request/ParameterDescriptor.java | 22 ---------------- .../restdocs/snippet/AbstractDescriptor.java | 25 ++++++++++++++++++- .../payload/RequestFieldsSnippetTests.java | 20 +++++++++++++++ .../restdocs/test/SnippetMatchers.java | 5 ++++ ...quest-fields-with-list-description.snippet | 12 +++++++++ 10 files changed, 64 insertions(+), 70 deletions(-) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 9d11f9156..cb7e9909a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -29,8 +29,6 @@ public class LinkDescriptor extends AbstractDescriptor { private final String rel; - private String description; - private boolean optional; /** @@ -42,17 +40,6 @@ protected LinkDescriptor(String rel) { this.rel = rel; } - /** - * Specifies the description of the link - * - * @param description The link's description - * @return {@code this} - */ - public final LinkDescriptor description(String description) { - this.description = description; - return this; - } - /** * Marks the link as optional * @@ -72,15 +59,6 @@ public final String getRel() { return this.rel; } - /** - * Returns the description for the link - * - * @return the link description - */ - public final String getDescription() { - return this.description; - } - /** * Returns {@code true} if the described link is optional, otherwise {@code false} * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 006327bb0..d6b225e05 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -75,7 +75,7 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getRel()); - Assert.hasText(descriptor.getDescription()); + Assert.notNull(descriptor.getDescription()); this.descriptorsByRel.put(descriptor.getRel(), descriptor); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 970cd7ec1..1aea09df4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -56,7 +56,7 @@ protected AbstractFieldsSnippet(String type, List descriptors, super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); - Assert.hasText(descriptor.getDescription()); + Assert.notNull(descriptor.getDescription()); } this.fieldDescriptors = descriptors; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index b9cac2dfa..1edd352ab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -34,8 +34,6 @@ public class FieldDescriptor extends AbstractDescriptor { private boolean optional; - private String description; - /** * Creates a new {@code FieldDescriptor} describing the field with the given * {@code path}. @@ -68,17 +66,6 @@ public final FieldDescriptor optional() { return this; } - /** - * Specifies the description of the field - * - * @param description The field's description - * @return {@code this} - */ - public final FieldDescriptor description(String description) { - this.description = description; - return this; - } - /** * Returns the path of the field described by this descriptor * @@ -106,13 +93,4 @@ public final boolean isOptional() { return this.optional; } - /** - * Returns the description for the field - * - * @return the field description - */ - public final String getDescription() { - return this.description; - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index fcb8ae2b7..f0597fef8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -55,7 +55,7 @@ protected AbstractParametersSnippet(String snippetName, super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.hasText(descriptor.getName()); - Assert.hasText(descriptor.getDescription()); + Assert.notNull(descriptor.getDescription()); this.descriptorsByName.put(descriptor.getName(), descriptor); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 549500daa..a1a2840fc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -29,8 +29,6 @@ public class ParameterDescriptor extends AbstractDescriptor private final String name; - private String description; - /** * Creates a new {@code ParameterDescriptor} describing the parameter with the given * {@code name}. @@ -41,17 +39,6 @@ protected ParameterDescriptor(String name) { this.name = name; } - /** - * Specifies the description of the parameter - * - * @param description The parameter's description - * @return {@code this} - */ - public final ParameterDescriptor description(String description) { - this.description = description; - return this; - } - /** * Returns the name of the parameter being described by this descriptor * @@ -61,13 +48,4 @@ public final String getName() { return this.name; } - /** - * Returns the description of the parameter - * - * @return the description - */ - public final String getDescription() { - return this.description; - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index 931cd79ed..34efc7280 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -33,6 +33,8 @@ public abstract class AbstractDescriptor> { private Map attributes = new HashMap<>(); + private Object description; + /** * Adds the given {@code attributes} to the descriptor * @@ -40,13 +42,34 @@ public abstract class AbstractDescriptor> { * @return the descriptor */ @SuppressWarnings("unchecked") - public T attributes(Attribute... attributes) { + public final T attributes(Attribute... attributes) { for (Attribute attribute : attributes) { this.attributes.put(attribute.getKey(), attribute.getValue()); } return (T) this; } + /** + * Specifies the description + * + * @param description the description + * @return the descriptor + */ + @SuppressWarnings("unchecked") + public final T description(Object description) { + this.description = description; + return (T) this; + } + + /** + * Returns the description + * + * @return the description + */ + public final Object getDescription() { + return this.description; + } + /** * Returns the descriptor's attributes * diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 819dd592e..7e603a5b3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -179,6 +179,26 @@ public void requestFieldsWithCustomAttributes() throws IOException { .request("http://localhost").content("{\"a\": \"foo\"}").build()); } + @Test + public void requestFieldsWithListDescription() throws IOException { + this.snippet.expectRequestFields("request-fields-with-list-description") + .withContents( + tableWithHeader("Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("request-fields")).thenReturn( + snippetResource("request-fields-with-list-description")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( + Arrays.asList("one", "two")))) + .document(new OperationBuilder("request-fields-with-list-description", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").content("{\"a\": \"foo\"}").build()); + } + @Test public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request").withContents( // diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index b174cf40d..444b2b53f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -191,6 +191,11 @@ public AsciidoctorTableMatcher row(String... entries) { this.addLine(-1, ""); return this; } + + public AsciidoctorTableMatcher configuration(String configuration) { + this.addLine(0, configuration); + return this; + } } public static class SnippetMatcher extends BaseMatcher { diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet new file mode 100644 index 000000000..e48204967 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet @@ -0,0 +1,12 @@ +[cols="1,1,1a"] +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{#description}} - {{.}} +{{/description}} + +{{/fields}} +|=== \ No newline at end of file From 26adbb10aee41c87918d1d42a2bcd985cab4c6f7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 13 Sep 2015 12:01:41 -0400 Subject: [PATCH 0145/1059] Add missing copyright header --- .../restdocs/snippet/ModelCreationException.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java index db93a778a..d36555273 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java @@ -1,3 +1,18 @@ +/* + * 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.snippet; import org.springframework.restdocs.operation.Operation; From d329e2da5d41d511e302cfd8ad31fec0a7ec3e8e Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Thu, 10 Sep 2015 00:30:11 +0200 Subject: [PATCH 0146/1059] Use -u 'username:password' for basic auth in the curl request snippet Previously, when a request was made that used basic auth, the curl snippet would configure the authentication header with the Base64-encoded header. This commit updates the snippet to use the more human-friendly -u option to provide the username and password in place of the authentication header. Closes gh-122 --- spring-restdocs-core/build.gradle | 1 + .../restdocs/curl/CurlRequestSnippet.java | 24 ++++++++++++++++++- .../curl/CurlRequestSnippetTests.java | 12 ++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 5f5be34ff..335edf284 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -27,6 +27,7 @@ dependencies { compile 'junit:junit' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' + compile 'commons-codec:commons-codec:1.10' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 018cb0f1a..7cebe503a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -29,12 +29,14 @@ import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; /** * A {@link Snippet} that documents the curl command for a request. * * @author Andy Wilkinson + * @author Paul-Christian Volkmer * @see CurlDocumentation#curlRequest() * @see CurlDocumentation#curlRequest(Map) */ @@ -73,6 +75,7 @@ private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeOptionToIncludeHeadersInOutput(printer); + writeHttpBasicAuthorization(operation.getRequest(), printer); writeHttpMethodIfNecessary(operation.getRequest(), printer); writeHeaders(operation.getRequest(), printer); writePartsIfNecessary(operation.getRequest(), printer); @@ -92,9 +95,24 @@ private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter wr } } + private void writeHttpBasicAuthorization(OperationRequest request, PrintWriter writer) { + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + if (isAuthBasicHeader(entry.getKey(), header)) { + String auth = new String(Base64Utils.decodeFromString(header.replace("Basic", "").trim())); + writer.print(String.format(" -u '%s'", auth)); + break; + } + } + } + } + private void writeHeaders(OperationRequest request, PrintWriter writer) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { + if (isAuthBasicHeader(entry.getKey(), header)) { + continue; + } writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } @@ -142,4 +160,8 @@ private boolean isPutOrPost(OperationRequest request) { || HttpMethod.POST.equals(request.getMethod()); } -} \ No newline at end of file + private boolean isAuthBasicHeader(String key, String value) { + return ("Authorization".equals(key) && value.startsWith("Basic")); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 96d6927a4..8f69267a0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -36,6 +36,7 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import org.springframework.util.Base64Utils; /** * Tests for {@link CurlRequestSnippet} @@ -44,6 +45,7 @@ * @author Yann Le Guern * @author Dmitriy Mayboroda * @author Jonathan Pearlin + * @author Paul-Christian Volkmer */ public class CurlRequestSnippetTests { @@ -257,4 +259,14 @@ public void customAttributes() throws IOException { .request("http://localhost/foo").build()); } + @Test + public void httpBasicAuthorizationHeader() throws IOException { + this.snippet.expectCurlRequest("get-request") + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); + new CurlRequestSnippet().document(new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) + .build()); + } + } From ce26bd4b1ad1f66e784fe60621ecbe06fadc7777 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 16 Sep 2015 17:39:24 -0400 Subject: [PATCH 0147/1059] Polish contribution - Make commons-codec an optional dependency - Isolate knowledge of basic auth header to code that produces the -u option Closes gh-120 --- build.gradle | 1 + spring-restdocs-core/build.gradle | 2 +- .../restdocs/curl/CurlRequestSnippet.java | 50 +++++++++---------- .../curl/CurlRequestSnippetTests.java | 13 +++-- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index b593dfb90..a849cd988 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ subprojects { dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' dependency 'com.samskivert:jmustache:1.10' + dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' dependency 'junit:junit:4.12' diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 335edf284..2de27164d 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -27,11 +27,11 @@ dependencies { compile 'junit:junit' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' - compile 'commons-codec:commons-codec:1.10' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' optional 'javax.validation:validation-api' + optional 'commons-codec:commons-codec' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 7cebe503a..0802bdf41 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Map.Entry; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -74,10 +75,10 @@ private String getUrl(Operation operation) { private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); - writeOptionToIncludeHeadersInOutput(printer); - writeHttpBasicAuthorization(operation.getRequest(), printer); + writeIncludeHeadersInOutputOption(printer); + HttpHeaders headers = writeUserOptionIfNecessary(operation.getRequest(), printer); writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest(), printer); + writeHeaders(headers, printer); writePartsIfNecessary(operation.getRequest(), printer); writeContent(operation.getRequest(), printer); @@ -85,34 +86,37 @@ private String getOptions(Operation operation) { return command.toString(); } - private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { - if (!HttpMethod.GET.equals(request.getMethod())) { - writer.print(String.format(" -X %s", request.getMethod())); + private HttpHeaders writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + HttpHeaders headers = new HttpHeaders(); + headers.putAll(request.getHeaders()); + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + if (isAuthorizationBasicHeader(authorization)) { + String credentials = new String(Base64Utils.decodeFromString(authorization + .substring(5).trim())); + writer.print(String.format(" -u '%s'", credentials)); + headers.remove(HttpHeaders.AUTHORIZATION); } + return headers; } - private void writeHttpBasicAuthorization(OperationRequest request, PrintWriter writer) { - for (Entry> entry : request.getHeaders().entrySet()) { - for (String header : entry.getValue()) { - if (isAuthBasicHeader(entry.getKey(), header)) { - String auth = new String(Base64Utils.decodeFromString(header.replace("Basic", "").trim())); - writer.print(String.format(" -u '%s'", auth)); - break; - } - } + private boolean isAuthorizationBasicHeader(String header) { + return header != null && header.startsWith("Basic"); + } + + private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { + if (!HttpMethod.GET.equals(request.getMethod())) { + writer.print(String.format(" -X %s", request.getMethod())); } } - private void writeHeaders(OperationRequest request, PrintWriter writer) { - for (Entry> entry : request.getHeaders().entrySet()) { + private void writeHeaders(HttpHeaders headers, PrintWriter writer) { + for (Entry> entry : headers.entrySet()) { for (String header : entry.getValue()) { - if (isAuthBasicHeader(entry.getKey(), header)) { - continue; - } writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } @@ -160,8 +164,4 @@ private boolean isPutOrPost(OperationRequest request) { || HttpMethod.POST.equals(request.getMethod()); } - private boolean isAuthBasicHeader(String key, String value) { - return ("Authorization".equals(key) && value.startsWith("Basic")); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 8f69267a0..2e15a1161 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -260,12 +260,15 @@ public void customAttributes() throws IOException { } @Test - public void httpBasicAuthorizationHeader() throws IOException { - this.snippet.expectCurlRequest("get-request") - .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet().document(new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + this.snippet.expectCurlRequest("basic-auth").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -u 'user:secret'")); + new CurlRequestSnippet().document(new OperationBuilder("basic-auth", this.snippet + .getOutputDirectory()) .request("http://localhost/foo") - .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) .build()); } From 9fe4442fcd62832247dc297b7093864d412acb76 Mon Sep 17 00:00:00 2001 From: izeye Date: Thu, 17 Sep 2015 21:10:50 +0900 Subject: [PATCH 0148/1059] Fix typo in ConstraintDescriptions' javadoc Closes gh-127 --- .../restdocs/constraints/ConstraintDescriptions.java | 2 +- .../restdocs/constraints/ValidatorConstraintResolver.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java index 0161b6d57..23d55d8c6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java @@ -48,7 +48,7 @@ public ConstraintDescriptions(Class clazz) { /** * Create a new {@code ConstraintDescriptions} for the given {@code clazz}. - * Constraints will be resolved using the given {@link constraintResolver} and + * Constraints will be resolved using the given {@code constraintResolver} and * descriptions will be resolved using a * {@link ResourceBundleConstraintDescriptionResolver}. * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index 2a638ed62..041f2bd3d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -42,7 +42,7 @@ public class ValidatorConstraintResolver implements ConstraintResolver { /** * Creates a new {@code ValidatorConstraintResolver} that will use a {@link Validator} - * in its default configurationto resolve constraints. + * in its default configuration to resolve constraints. * * @see Validation#buildDefaultValidatorFactory() * @see ValidatorFactory#getValidator() From 387257ba6f2b426bf0671292a7fc470fe458495f Mon Sep 17 00:00:00 2001 From: Oliver Trosien Date: Thu, 17 Sep 2015 17:08:11 +0200 Subject: [PATCH 0149/1059] Fix typos in sample asciidoc Closes gh-129 --- .../src/main/asciidoc/api-guide.adoc | 4 ++-- .../src/main/asciidoc/api-guide.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 002b8a441..50498df41 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -76,9 +76,9 @@ include::{snippets}/error-example/http-response.adoc[] RESTful Notes uses hypermedia and resources include links to other resources in their responses. Responses are in http://stateless.co/hal_specification.html[Hypertext Application -Language (HAL)] format. Links can be found benath the `_links` key. Users of the API should -not created URIs themselves, instead they should use the above-described links to navigate from resource to resource. +Language (HAL)] format. Links can be found beneath the `_links` key. Users of the API should +not create URIs themselves, instead they should use the above-described links to navigate [[resources]] = Resources diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index f0ca947eb..58c579bbc 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -76,9 +76,9 @@ include::{snippets}/error-example/http-response.adoc[] RESTful Notes uses hypermedia and resources include links to other resources in their responses. Responses are in http://stateless.co/hal_specification.html[Hypertext Application -Language (HAL)] format. Links can be found benath the `_links` key. Users of the API should -not created URIs themselves, instead they should use the above-described links to navigate from resource to resource. +Language (HAL)] format. Links can be found beneath the `_links` key. Users of the API should +not create URIs themselves, instead they should use the above-described links to navigate [[resources]] = Resources From 5cd25897ce7b310f2af6eb62ee5c2194f3a923f0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Sep 2015 15:50:56 +0100 Subject: [PATCH 0150/1059] Update references to RestDocumentationConfigurer in the documentation Following the refactoring, the class is now named RestDocumentationMockMvcConfigurer. --- docs/src/docs/asciidoc/configuration.adoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 0dd0779bb..ae1b601f8 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -21,8 +21,8 @@ The default configuration for URIs documented by Spring REST Docs is: |`8080` |=== -This configuration is applied by `RestDocumentationConfigurer`. You can use its API to -change one or more of the defaults to suit your needs: +This configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use its API +to change one or more of the defaults to suit your needs: [source,java,indent=0] ---- @@ -39,7 +39,7 @@ TIP: To configure a request's context path, use the `contextPath` method on The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same default for the snippets that it generates. If you require an encoding other than `UTF-8`, -use `RestDocumentationConfigurer` to configure it: +use `RestDocumentationMockMvcConfigurer` to configure it: [source,java,indent=0] ---- @@ -57,9 +57,9 @@ Three snippets are produced by default: - `http-request` - `http-response` -This default configuration is applied by `RestDocumentationConfigurer`. You can use its -API to change the configuration. For example, to only produce the `curl-request` snippet -by default: +This default configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use +its API to change the configuration. For example, to only produce the `curl-request` +snippet by default: [source,java,indent=0] ---- From f3b83f977eaa9e62516ea62a5b53faeadc5acbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJeremy?= Date: Mon, 21 Sep 2015 20:57:20 -0600 Subject: [PATCH 0151/1059] Add basic support for using bracket notation in JSON field paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, only dot notation JSON field paths were supported. Using dot notation, each key is separated by a '.'. This makes it impossible to reference a field where the key itself contains a dot. This commit adds basic bracket notation support to JsonFieldPath. With this commit, existing functionality remains as is, but users can now use a basic form of JsonPath bracket notation. This enables the use of Json keys with embedded dots. i.e. something like : { "a.b" : "one" } .andDo(document("example",responseFields(fieldWithPath("['a.b']")… Closes gh-132 --- .../docs/asciidoc/documenting-your-api.adoc | 26 +++++++++-- .../restdocs/payload/JsonFieldPath.java | 46 ++++++++++++------- .../restdocs/payload/JsonFieldPathTests.java | 16 +++++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 1bbc2b05a..0a07f13ea 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -92,8 +92,10 @@ must be compatible with `application/xml`. [[documenting-your-api-request-response-payloads-json-field-paths]] ===== JSON field paths -JSON field paths use `.` to descend into a child object and `[]` to identify an array. For -example, with this JSON payload: +JSON field paths use `.` or bracket notation to descend into a child object and `[]` to +identify an array. Using bracket notation enables the use of `.` within a key name. + +For example, with this JSON payload: [source,json,indent=0] ---- @@ -109,7 +111,8 @@ example, with this JSON payload: { "d":"three" } - ] + ], + "e.dot" : "four" } } ---- @@ -126,6 +129,15 @@ The following paths are all present: |`a.b` |An array containing three objects +|`['a']['b']` +|An array containing three objects + +|`a['b']` +|An array containing three objects + +|`['a'].b` +|An array containing three objects + |`a.b[]` |An array containing three objects @@ -134,6 +146,14 @@ The following paths are all present: |`a.b[].d` |The string `three` + +|`a['e.dot']` +|The string `four` + +|`['a']['e.dot']` +|The string `four` + + |=== diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index ea44aaff2..e546a1157 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.payload; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,10 +25,15 @@ * A path that identifies a field in a JSON payload * * @author Andy Wilkinson + * @author Jeremy Rickard * */ final class JsonFieldPath { + private static final Pattern BRACKETS_AND_ARRAY_PATTERN = Pattern + .compile("\\[\'(.+?)\'\\]|\\[([0-9]+|\\*){0,1}\\]"); + + private static final Pattern ARRAY_INDEX_PATTERN = Pattern .compile("\\[([0-9]+|\\*){0,1}\\]"); @@ -76,31 +81,38 @@ static boolean matchesSingleValue(List segments) { } private static List extractSegments(String path) { - Matcher matcher = ARRAY_INDEX_PATTERN.matcher(path); - StringBuilder buffer = new StringBuilder(); + Matcher matcher = BRACKETS_AND_ARRAY_PATTERN.matcher(path); + int previous = 0; + + List tokens = new ArrayList<>(); while (matcher.find()) { - appendWithSeparatorIfNecessary(buffer, - path.substring(previous, matcher.start(0))); - appendWithSeparatorIfNecessary(buffer, matcher.group()); + if (previous != matcher.start()) { + tokens.addAll(expandToken(path.substring(previous, matcher.start()))); + } + if (matcher.group(1) != null) { + tokens.add(matcher.group(1)); + } else { + tokens.add(matcher.group()); + } previous = matcher.end(0); } + if (previous < path.length()) { - appendWithSeparatorIfNecessary(buffer, path.substring(previous)); + tokens.addAll(expandToken(path.substring(previous))); } - String processedPath = buffer.toString(); - - return Arrays.asList(processedPath.indexOf('.') > -1 ? processedPath.split("\\.") - : new String[] { processedPath }); + return tokens; } - private static void appendWithSeparatorIfNecessary(StringBuilder buffer, - String toAppend) { - if (buffer.length() > 0 && (buffer.lastIndexOf(".") != buffer.length() - 1) - && !toAppend.startsWith(".")) { - buffer.append("."); + private static List expandToken(String token) { + String[] tokens = token.split("\\."); + List expandedTokens = new ArrayList<>(); + for (String aToken : tokens) { + if (aToken.length() > 0) { + expandedTokens.add(aToken); + } } - buffer.append(toAppend); + return expandedTokens; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index f94beee49..f219abcec 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -27,6 +27,7 @@ * Tests for {@link JsonFieldPath} * * @author Andy Wilkinson + * @author Jeremy Rickard */ public class JsonFieldPathTests { @@ -110,4 +111,19 @@ public void compilationOfPathStartingWithAnArray() { contains("[]", "a", "b", "c")); } + @Test + public void compilationOfMultipleElementPathWithBrackets() { + assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments(), contains("a", "b", "c")); + } + + @Test + public void compilationOfMultipleElementPathWithAndWithoutBrackets() { + assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments(), contains("a", "[]", "b", "c")); + } + + @Test + public void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { + assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments(), contains("a.key", "[]", "b", "c")); + } + } From 42aa9962779b558106b44f23b9cc9cc88a6abc6b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Sep 2015 16:58:51 +0100 Subject: [PATCH 0152/1059] Polish JSON field path bracket notation contribution Closes gh-131 --- .../docs/asciidoc/documenting-your-api.adoc | 12 +++++--- .../restdocs/payload/JsonFieldPath.java | 30 +++++++++---------- .../payload/JsonFieldProcessorTests.java | 24 +++++++++++---- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 0a07f13ea..7fd84cdf3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -92,10 +92,14 @@ must be compatible with `application/xml`. [[documenting-your-api-request-response-payloads-json-field-paths]] ===== JSON field paths -JSON field paths use `.` or bracket notation to descend into a child object and `[]` to -identify an array. Using bracket notation enables the use of `.` within a key name. - -For example, with this JSON payload: +JSON field paths use either dot notation or bracket notation. Dot notation uses '.' to +separate each key in the path; `a.b`, for example. Bracket notation wraps each key in +square brackets and single quotes; `['a']['b']`, for example. In either case, `[]` is used +to identify an array. Dot notation is more concise, but using bracket notation enables the +use of `.` within a key name; `['a.b']`, for example. The two different notations can be +used in the same path; `a['b']`, for example. + +With this JSON payload: [source,json,indent=0] ---- diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index e546a1157..aca86bcb5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -33,7 +33,6 @@ final class JsonFieldPath { private static final Pattern BRACKETS_AND_ARRAY_PATTERN = Pattern .compile("\\[\'(.+?)\'\\]|\\[([0-9]+|\\*){0,1}\\]"); - private static final Pattern ARRAY_INDEX_PATTERN = Pattern .compile("\\[([0-9]+|\\*){0,1}\\]"); @@ -85,34 +84,35 @@ private static List extractSegments(String path) { int previous = 0; - List tokens = new ArrayList<>(); + List segments = new ArrayList<>(); while (matcher.find()) { if (previous != matcher.start()) { - tokens.addAll(expandToken(path.substring(previous, matcher.start()))); + segments.addAll(extractDotSeparatedSegments(path.substring(previous, + matcher.start()))); } if (matcher.group(1) != null) { - tokens.add(matcher.group(1)); - } else { - tokens.add(matcher.group()); + segments.add(matcher.group(1)); + } + else { + segments.add(matcher.group()); } previous = matcher.end(0); } if (previous < path.length()) { - tokens.addAll(expandToken(path.substring(previous))); + segments.addAll(extractDotSeparatedSegments(path.substring(previous))); } - return tokens; + return segments; } - private static List expandToken(String token) { - String[] tokens = token.split("\\."); - List expandedTokens = new ArrayList<>(); - for (String aToken : tokens) { - if (aToken.length() > 0) { - expandedTokens.add(aToken); + private static List extractDotSeparatedSegments(String path) { + List segments = new ArrayList<>(); + for (String segment : path.split("\\.")) { + if (segment.length() > 0) { + segments.add(segment); } } - return expandedTokens; + return segments; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index bc61a3388..35ed2d64e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -111,7 +111,8 @@ public void extractFromItemsInNestedArray() { List>> alpha = Arrays.asList( Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload), + assertThat( + this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload), equalTo((Object) Arrays.asList("1", "2", "3"))); } @@ -124,15 +125,15 @@ public void extractArraysFromItemsInNestedArray() { List>> alpha = Arrays.asList( Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload), - equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), - Arrays.asList(4)))); + assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), + payload), equalTo((Object) Arrays.asList(Arrays.asList(1, 2), + Arrays.asList(3), Arrays.asList(4)))); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentTopLevelField() { - this.fieldProcessor - .extract(JsonFieldPath.compile("a"), new HashMap()); + this.fieldProcessor.extract(JsonFieldPath.compile("a"), + new HashMap()); } @Test(expected = FieldDoesNotExistException.class) @@ -216,6 +217,17 @@ public void removeItemsInNestedArray() throws IOException { assertThat(payload.size(), equalTo(0)); } + @Test + public void extractNestedEntryWithDotInKeys() throws IOException { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a.key", alpha); + alpha.put("b.key", "bravo"); + assertThat(this.fieldProcessor.extract( + JsonFieldPath.compile("['a.key']['b.key']"), payload), + equalTo((Object) "bravo")); + } + private Map createEntry(String... pairs) { Map entry = new HashMap<>(); for (String pair : pairs) { From 50514293a8ad06763f3d516a0c512f192769dbe9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Sep 2015 17:45:34 +0100 Subject: [PATCH 0153/1059] Add an integration test for custom context path configuration Closes gh-135 --- ...ckMvcRestDocumentationIntegrationTests.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 6e015cfde..1ea71c403 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -40,6 +40,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; @@ -347,6 +348,23 @@ public void customSnippetTemplate() throws Exception { "Access the index using curl"))))); } + @Test + public void customContextPath() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform( + get("/custom/").contextPath("/custom").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("custom-context-path")); + assertThat( + new File("build/generated-snippets/custom-context-path/curl-request.adoc"), + is(snippet() + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); From 2fc0420aefcdb9df1ca6d90c4748cd48e615d0af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 10:06:13 +0100 Subject: [PATCH 0154/1059] =?UTF-8?q?Document=20HTTP=20request=20snippet?= =?UTF-8?q?=E2=80=99s=20special=20treatment=20of=20the=20Host=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For an HTTP 1.1 request to be valid it must contain a Host header. To ensure that the HTTP request snippet is a valid HTTP 1.1 request a Host header will always be included. If one is not present in the request that’s being documented, the snippet will generate one automatically. A side-effect of this is that a preprocessor that removes the Host header will have no effect on the HTTP request snippet. This commit updates the documentation to describe this behaviour. Closes gh-134 --- .../src/docs/asciidoc/customizing-requests-and-responses.adoc | 4 ++++ docs/src/docs/asciidoc/documenting-your-api.adoc | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index f88a58795..d88fb06d6 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -77,6 +77,10 @@ different replacement can also be specified if you wish. `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. +NOTE: For an HTTP 1.1 request to be valid it must contain a `Host` header. Therefore, +irrespective of any preprocessing, the default HTTP request snippet will always contain a +`Host` header. + [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7fd84cdf3..e0e477f34 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -388,7 +388,8 @@ call that is being documented | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being -documented + documented. HTTP 1.1 requires a `Host` header. If you do not provide one via the + `MockMvc` API the snippet will add one automatically. | `http-response.adoc` | Contains the HTTP response that was returned From ccb5f3d191a38a59a7cead01fc2403790d2874e0 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Sun, 13 Sep 2015 15:58:43 +0200 Subject: [PATCH 0155/1059] Configure the build to use Checkstyle This commit adds checkstyle plugin and provides a simple config file. Closes gh-125 --- build.gradle | 5 + checkstyle.xml | 92 +++++++++++++++++++ .../com/example/ExampleApplicationTests.java | 4 +- 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 checkstyle.xml diff --git a/build.gradle b/build.gradle index a849cd988..7f849e632 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ subprojects { apply plugin: 'propdeps-eclipse' apply plugin: 'propdeps-maven' apply plugin: 'maven' + apply plugin: 'checkstyle' apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" sourceCompatibility = 1.7 @@ -72,6 +73,10 @@ subprojects { } } + checkstyle { + configFile = rootProject.file('checkstyle.xml') + } + configurations { jacoco } diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 000000000..4e7653a24 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java index cc6c64064..d94622f5f 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -27,7 +27,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class ExampleApplicationTests { - + // tag::mock-mvc-setup[] @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); @@ -44,4 +44,4 @@ public void setUp() { .build(); } // end::mock-mvc-setup[] -} \ No newline at end of file +} From 130a230105fd5b742ac60bf9d61ac3890e4cd87f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 11:36:01 +0100 Subject: [PATCH 0156/1059] Configure checkstyle and update code to comply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit configures Checkstyle and updates the build to comply with that configuration. The Eclipse JDT metadata has also been updated so that the output of Eclipse’s code formatting and clean-up is compliant with Checkstyle. The use of the latest version (6.10.1) of Checkstyle has required an upgrade to Gradle 2.7. Gradle hardcode’s the name of the Checkstyle’s main class which has changed between version 5 (Gradle’s default) and version 6. Closes gh-119 --- build.gradle | 37 ++- checkstyle.xml | 92 ------ config/checkstyle/checkstyle-header.txt | 17 + .../checkstyle/checkstyle-import-control.xml | 12 + config/checkstyle/checkstyle-suppressions.xml | 8 + config/checkstyle/checkstyle.xml | 156 +++++++++ .../eclipse}/org.eclipse.jdt.core.prefs | 94 +++++- config/eclipse/org.eclipse.jdt.ui.prefs | 125 +++++++ gradle/wrapper/gradle-wrapper.jar | Bin 52266 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 +- .../.settings/org.eclipse.jdt.core.prefs | 1 + .../.settings/org.eclipse.jdt.ui.prefs | 2 +- .../.settings/org.eclipse.jdt.ui.prefs | 2 +- .../.settings/org.eclipse.jdt.ui.prefs | 40 +-- .../restdocs/RestDocumentation.java | 2 +- .../restdocs/RestDocumentationContext.java | 14 +- .../restdocs/constraints/Constraint.java | 14 +- .../ConstraintDescriptionResolver.java | 6 +- .../constraints/ConstraintDescriptions.java | 17 +- .../constraints/ConstraintResolver.java | 4 +- ...ceBundleConstraintDescriptionResolver.java | 11 +- .../ValidatorConstraintResolver.java | 7 +- .../restdocs/constraints/package-info.java | 3 +- .../restdocs/curl/CurlRequestSnippet.java | 2 +- .../restdocs/curl/package-info.java | 3 +- .../restdocs/http/HttpDocumentation.java | 4 +- .../restdocs/http/HttpRequestSnippet.java | 4 +- .../restdocs/http/HttpResponseSnippet.java | 5 +- .../restdocs/http/package-info.java | 3 +- .../hypermedia/AbstractJsonLinkExtractor.java | 7 +- .../hypermedia/AtomLinkExtractor.java | 5 +- .../restdocs/hypermedia/HalLinkExtractor.java | 5 +- .../hypermedia/HypermediaDocumentation.java | 19 +- .../restdocs/hypermedia/Link.java | 8 +- .../restdocs/hypermedia/LinkDescriptor.java | 19 +- .../restdocs/hypermedia/LinksSnippet.java | 12 +- .../restdocs/hypermedia/package-info.java | 3 +- .../restdocs/operation/Operation.java | 10 +- .../restdocs/operation/OperationRequest.java | 18 +- .../operation/OperationRequestPart.java | 14 +- .../restdocs/operation/OperationResponse.java | 8 +- .../restdocs/operation/Parameters.java | 8 +- .../restdocs/operation/StandardOperation.java | 8 +- .../operation/StandardOperationRequest.java | 4 +- .../StandardOperationRequestPart.java | 4 +- .../operation/StandardOperationResponse.java | 4 +- .../restdocs/operation/package-info.java | 3 +- .../operation/preprocess/ContentModifier.java | 6 +- ...ContentModifyingOperationPreprocessor.java | 4 +- ...elegatingOperationRequestPreprocessor.java | 4 +- ...legatingOperationResponsePreprocessor.java | 4 +- .../HeaderRemovingOperationPreprocessor.java | 4 +- .../LinkMaskingContentModifier.java | 4 +- .../preprocess/OperationPreprocessor.java | 10 +- .../OperationRequestPreprocessor.java | 4 +- .../PatternReplacingContentModifier.java | 4 +- .../operation/preprocess/Preprocessors.java | 20 +- .../PrettyPrintingContentModifier.java | 2 +- .../operation/preprocess/package-info.java | 3 +- .../restdocs/package-info.java | 5 +- .../payload/AbstractFieldsSnippet.java | 14 +- .../restdocs/payload/ContentHandler.java | 12 +- .../restdocs/payload/FieldDescriptor.java | 25 +- .../payload/FieldDoesNotExistException.java | 4 +- .../payload/FieldTypeRequiredException.java | 4 +- .../restdocs/payload/JsonContentHandler.java | 2 +- .../restdocs/payload/JsonFieldPath.java | 4 +- .../restdocs/payload/JsonFieldProcessor.java | 8 +- .../restdocs/payload/JsonFieldType.java | 39 ++- .../payload/JsonFieldTypeResolver.java | 4 +- .../payload/PayloadDocumentation.java | 46 +-- .../payload/PayloadHandlingException.java | 4 +- .../payload/RequestFieldsSnippet.java | 6 +- .../payload/ResponseFieldsSnippet.java | 7 +- .../restdocs/payload/XmlContentHandler.java | 4 +- .../restdocs/payload/package-info.java | 3 +- .../request/AbstractParametersSnippet.java | 26 +- .../restdocs/request/ParameterDescriptor.java | 10 +- .../request/PathParametersSnippet.java | 4 +- .../request/RequestDocumentation.java | 12 +- .../request/RequestParametersSnippet.java | 4 +- .../restdocs/request/package-info.java | 3 +- .../restdocs/snippet/AbstractDescriptor.java | 15 +- .../restdocs/snippet/Attributes.java | 26 +- .../snippet/ModelCreationException.java | 11 +- ...cumentationContextPlaceholderResolver.java | 10 +- .../restdocs/snippet/Snippet.java | 4 +- .../restdocs/snippet/SnippetException.java | 5 +- .../snippet/StandardWriterResolver.java | 8 +- .../restdocs/snippet/TemplatedSnippet.java | 10 +- .../restdocs/snippet/WriterResolver.java | 6 +- .../restdocs/snippet/package-info.java | 3 +- .../StandardTemplateResourceResolver.java | 3 +- .../restdocs/templates/TemplateEngine.java | 4 +- .../templates/TemplateResourceResolver.java | 6 +- .../templates/mustache/MustacheTemplate.java | 4 +- .../mustache/MustacheTemplateEngine.java | 5 +- .../templates/mustache/package-info.java | 3 +- .../restdocs/templates/package-info.java | 3 +- .../ConstraintDescriptionsTests.java | 10 +- ...dleConstraintDescriptionResolverTests.java | 26 +- .../ValidatorConstraintResolverTests.java | 24 +- .../curl/CurlRequestSnippetTests.java | 20 +- .../http/HttpRequestSnippetTests.java | 56 ++-- .../http/HttpResponseSnippetTests.java | 36 +-- .../ContentTypeLinkExtractorTests.java | 9 +- .../LinkExtractorsPayloadTests.java | 8 +- .../hypermedia/LinksSnippetTests.java | 65 ++-- ...ntModifyingOperationPreprocessorTests.java | 16 +- ...tingOperationRequestPreprocessorTests.java | 27 +- ...ingOperationResponsePreprocessorTests.java | 28 +- ...derRemovingOperationPreprocessorTests.java | 14 +- .../LinkMaskingContentModifierTests.java | 23 +- .../PatternReplacingContentModifierTests.java | 10 +- .../PrettyPrintingContentModifierTests.java | 8 +- .../restdocs/payload/JsonFieldPathTests.java | 17 +- .../payload/JsonFieldProcessorTests.java | 10 +- .../payload/JsonFieldTypeResolverTests.java | 13 +- .../payload/RequestFieldsSnippetTests.java | 79 +++-- .../payload/ResponseFieldsSnippetTests.java | 81 +++-- .../request/PathParametersSnippetTests.java | 50 +-- .../RequestParametersSnippetTests.java | 60 ++-- ...tationContextPlaceholderResolverTests.java | 8 +- .../snippet/StandardWriterResolverTests.java | 12 +- ...StandardTemplateResourceResolverTests.java | 11 +- .../restdocs/test/ExpectedSnippet.java | 40 +-- .../restdocs/test/OperationBuilder.java | 22 +- .../restdocs/test/SnippetMatchers.java | 42 ++- .../.settings/org.eclipse.jdt.core.prefs | 305 ------------------ .../.settings/org.eclipse.jdt.ui.prefs | 40 +-- .../restdocs/mockmvc/AbstractConfigurer.java | 5 +- .../mockmvc/AbstractNestedConfigurer.java | 4 +- .../restdocs/mockmvc/IterableEnumeration.java | 7 +- .../MockMvcOperationRequestFactory.java | 8 +- .../MockMvcOperationResponseFactory.java | 4 +- .../mockmvc/MockMvcRestDocumentation.java | 14 +- .../restdocs/mockmvc/NestedConfigurer.java | 8 +- .../RestDocumentationMockMvcConfigurer.java | 22 +- .../RestDocumentationRequestBuilders.java | 36 +-- .../RestDocumentationResultHandler.java | 12 +- .../restdocs/mockmvc/SnippetConfigurer.java | 8 +- .../restdocs/mockmvc/UriConfigurer.java | 24 +- .../restdocs/mockmvc/package-info.java | 3 +- .../MockMvcOperationRequestFactoryTests.java | 47 ++- ...kMvcRestDocumentationIntegrationTests.java | 72 +++-- .../RestDocumentationConfigurerTests.java | 12 +- ...RestDocumentationRequestBuildersTests.java | 25 +- .../restdocs/mockmvc/test/StubMvcResult.java | 154 --------- .../mockmvc/test/TestRequestBuilders.java | 36 --- 150 files changed, 1456 insertions(+), 1514 deletions(-) delete mode 100644 checkstyle.xml create mode 100644 config/checkstyle/checkstyle-header.txt create mode 100644 config/checkstyle/checkstyle-import-control.xml create mode 100644 config/checkstyle/checkstyle-suppressions.xml create mode 100644 config/checkstyle/checkstyle.xml rename {spring-restdocs-core/.settings => config/eclipse}/org.eclipse.jdt.core.prefs (79%) create mode 100644 config/eclipse/org.eclipse.jdt.ui.prefs delete mode 100644 spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs delete mode 100644 spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/StubMvcResult.java delete mode 100644 spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/TestRequestBuilders.java diff --git a/build.gradle b/build.gradle index 7f849e632..ac4c5afa3 100644 --- a/build.gradle +++ b/build.gradle @@ -47,8 +47,6 @@ subprojects { apply plugin: 'propdeps-eclipse' apply plugin: 'propdeps-maven' apply plugin: 'maven' - apply plugin: 'checkstyle' - apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" sourceCompatibility = 1.7 targetCompatibility = 1.7 @@ -73,8 +71,32 @@ subprojects { } } + test { + testLogging { + exceptionFormat "full" + } + } + + eclipseJdt { + inputFile = rootProject.file('config/eclipse/org.eclipse.jdt.core.prefs') + doLast { + project.file('.settings/org.eclipse.jdt.ui.prefs').withWriter { writer -> + writer << rootProject.file('config/eclipse/org.eclipse.jdt.ui.prefs').text + } + } + } + +} + +configure(subprojects - project(":docs")) { subproject -> + + apply plugin: 'checkstyle' + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + checkstyle { - configFile = rootProject.file('checkstyle.xml') + configFile = rootProject.file('config/checkstyle/checkstyle.xml') + configProperties = [ 'checkstyle.config.dir' : rootProject.file('config/checkstyle') ] + toolVersion = '6.10.1' } configurations { @@ -85,12 +107,6 @@ subprojects { jacoco 'org.jacoco:org.jacoco.agent::runtime' } - test { - testLogging { - exceptionFormat "full" - } - } - javadoc { description = "Generates project-level javadoc for use in -javadoc jar" options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED @@ -101,9 +117,6 @@ subprojects { options.addStringOption '-quiet' } - eclipseJdt.onlyIf { false } - cleanEclipseJdt.onlyIf { false } - task sourcesJar(type: Jar) { classifier = 'sources' from project.sourceSets.main.allSource diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index 4e7653a24..000000000 --- a/checkstyle.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/checkstyle/checkstyle-header.txt b/config/checkstyle/checkstyle-header.txt new file mode 100644 index 000000000..154aaa1a6 --- /dev/null +++ b/config/checkstyle/checkstyle-header.txt @@ -0,0 +1,17 @@ +^\Q/*\E$ +^\Q * Copyright \E20\d\d\-20\d\d\Q the original author or authors.\E$ +^\Q *\E$ +^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$ +^\Q * you may not use this file except in compliance with the License.\E$ +^\Q * You may obtain a copy of the License at\E$ +^\Q *\E$ +^\Q * http://www.apache.org/licenses/LICENSE-2.0\E$ +^\Q *\E$ +^\Q * Unless required by applicable law or agreed to in writing, software\E$ +^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ +^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$ +^\Q * See the License for the specific language governing permissions and\E$ +^\Q * limitations under the License.\E$ +^\Q */\E$ +^$ +^.*$ diff --git a/config/checkstyle/checkstyle-import-control.xml b/config/checkstyle/checkstyle-import-control.xml new file mode 100644 index 000000000..af08f1a2c --- /dev/null +++ b/config/checkstyle/checkstyle-import-control.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 000000000..f74d92343 --- /dev/null +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 000000000..117567d12 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs similarity index 79% rename from spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs rename to config/eclipse/org.eclipse.jdt.core.prefs index f96273611..5281c3b46 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.core.prefs +++ b/config/eclipse/org.eclipse.jdt.core.prefs @@ -10,14 +10,100 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.7 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=default +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=default +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.source=1.7 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 @@ -46,8 +132,8 @@ org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=1 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 @@ -100,7 +186,7 @@ org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=8 +org.eclipse.jdt.core.formatter.indentation.size=4 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert @@ -128,7 +214,7 @@ org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs new file mode 100644 index 000000000..c5e23c905 --- /dev/null +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -0,0 +1,125 @@ +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=true +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_functional_interfaces=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.insert_inferred_type_arguments=false +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=false +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=true +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=false +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_anonymous_class_creation=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=false +cleanup.use_this_for_non_static_field_access_only_if_necessary=false +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_type_arguments=false +cleanup_profile=_Spring REST Docs Cleanup Conventions +cleanup_settings_version=2 +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_Spring REST Docs Java Conventions +formatter_settings_version=12 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=false +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; +org.eclipse.jdt.ui.javadoc=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.ondemandthreshold=9999 +org.eclipse.jdt.ui.overrideannotation=true +org.eclipse.jdt.ui.staticondemandthreshold=9999 +org.eclipse.jdt.ui.text.custom_code_templates= +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=true +sp_cleanup.always_use_this_for_non_static_field_access=true +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.make_local_variable_final=false +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=false +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=false +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.organize_imports=true +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=true +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=true +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_blocks=true +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=true +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index b5166dad4d90021f6a0b45268c0755719f1d5cd4..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 100644 GIT binary patch delta 12509 zcmZvC1ymhPvn_7H-7UBWf?Ejg?(Xichu}ei92|nX1$TFMcM0weLGt(@_xo@D`(~}P zy4LRMUDY);Jw1J@C=om;8yrzi5*z{!1Ox^KButV)G#Zf{=ASl8-X$-ZTOIw<;oJcf zg9?T6&-h^k9g%1- z5RiK)5D*=Yd4+NXDrxni(Za-Rvw?ESKp|$65~$V`I7?J7-kEl8FO)Y+?A} z@F({CeIq>Vw+-&?BcE}@%z2E)Z>;&xj#SvowS)D`?ah3c5&GZ&;iV(j`S5|Oze4vl z;d2Y}4Y{nd#aZ*|Z(CUW!Gkhal`h*;b=9m~BC#iRkG9NEBePbgS(YmN$%EW#YDc2f zoNo)$Z0EmS*LqG@Hs6{QqpcpFBz?D@s=8MvL+ki*%rcEzHpjM@*05F)K9=V0W#!&0 z^HWuOH+$v8gTj;quUJNDq9Sm99-Tos=yKQTp*~+A2pI4a zI=VItKUfc%G)I2zoD@f}J0tk_v%shNkUZ0#v!X}M$!+Eh10sSz5rkJcuG4kn%D8kl zMGnUo-!Xwc2ZJbtoPR~{xjW8egwaKBCG+CE0@q7TKVtgsLN(SOdC1>>rEmmc-xA9q zz#OiqpZbhB$Ot45iLru&4Rwg6){D*h5H6<@1|@G2&eMgVg^_^$)1AH$Ai8t7P#%k% z8U{4owAVu01dtzmJGb%v1(D61BM>iMH3^+V_Q^sbsv2J5y{g}y#fwlWlRW4BX%NhaYkK+V z-QN#D)hse2r`xIXvBZgnq#$g1rH4bm}xK|E?6IqHmw_`t|DCj99A$!LsFsYaN@Oskycgi1-I0ll)k_&C+P zg?H^uW=&1ajEbf0;c-jL?ak`a1I;bK#DzV@2zisS?a5@vGj;cO>ZzBm&eOw`=vOlm z0CEQ3XhJV#1Zf07H`x#DBdB@rKt65;GvSXj2&D6rF-WR*G-Ionk3ev(e#M+5+;!44 z%na^&wd8#`Xz-75ru~meaAOyjM<8SR(i>CYPPk(TXE;2v9uhg6iW~Q!$z5CET7$-* zMOzN{B~f__$wl^BBdlK$FKLa{z_u2?LIRh7iyA@b3f}24&ZmaPDPv z>yB>9zGK^$U~a{VZqB~({#d;Sx#m%M@>C@#aJvugrB@%Ry}op&UpQ8!&xwml`i^d0 zPg=p81vblX%z~6|pZlzmjg3qyLAt%ORlXw)Tr!%h1u5M<2DR7-!&pVhP}x}OOt`yO zxUZK=rU|?TfYjJGb!LKC@4D~@KUv*12>v{#TW4SsE*^SnR!l=dp(wJW`$SMqmVC!? zvO}9APL&EjE@Knh8}pSq(3N@_EyE@WZ&ycsoHA4gXTA8&;=Wm9m5-jUL1V)*(GMZQ z@_>!qwy`?1(0QsiZzhGEwbZu3NpK~vMk;V5;jrlnkQ_pISYlp-YEt3|Id%8Tjm~`k zgJl?@xWI*Fl2^2wX@0a`@&vxD)2tK4m0fqizWCnCibz)80%5lVfMXRctoxd2%Bt?G zG{V09#$>6cw(~#^ighH+c7x43wx6RNSySPwa%6EIU2!ajL*%jGa0Y&I@#Za6&eY4B z@oa!a0WM2F8s^o6?#E0DLbB5WHqKFZ}ki)Mq;|}NaIMo(IX?~}~U(|QWLv1wfHKZNH&rdnW zt@v6`8p}PZ_^jT{z_i}*`3lrI<$*Cd zlj$Y%Nfgc8(2OT8n5#KgoMFD#ce&bLv@qmB6r;z^Tr;~CAt~gTn?Vp^j%8{hK=SA^ z0XvX#P|@R2n6g^*kijDa^lI@U~)J9gP zup4wUMVV$%o3U~mHpWR{XKORX2PuIc>838cg z@41%i2e9LM%!@~h)8fmjs}nDl%ypy5uT&nvuEHqb?m5-H=iSA279?dDaB&_C=ZG!O zoQR!3GwQ|;y1ZUk*Nlez(vn3D75`OK()G(0Q65RlGJttHYp*_>H+8@BPe8r z=&;7E!$vjB^*|M?4iV!QPI^;AFFdfss^pS|1xqmDXm3ZDy9S*fhGsc%K8FR24_2TJ zmVRNb#!OG06?c>jnnlf2X2sN43c_TJ>%mIv+^pD{X1jI#9FB5lruHDaY zV(j1PTKvfR+&Fl9<`Z|iZ7b%OK6o33id67O`~;8JF}@j5m}o2#Vby8Uc;ux?NnfWj zzLP+=M|Rk~AQ%U~8XtmQgyLYP`}8nzh0HFfy6FQfX?*n>VAZ zjansnC8Si<==3fkbHtUteb;j3D+i{AH!j@@Wix)ca&CvpcS1n$V)Arpa#DajYSw#v zw0QF5G+(p*0I2Z!qsgI~Z$dmp`^j-)zi%;94kP(rEULi!Pcd&8k=zT;xK1Z{HNmUHd;}2xsMLeJQ z74KpAb1$YY*kAzZ6DfW_KQ-Y>x1eiImjFm7VTDlbIVcz?s@8LU@1NG@yC8$ln=yL z95*g@=~eQH)E9vzYI5stkDx)oAm;3u$Qr{03 zx<%M-I_2PS_)TV}1v$Qs>(Akj?!ZBsdJStsjSkDL%`#&8J-^c)-7%(YfKS?tX^r}J z2J?=h?gxXVpGe_JxsCX}KO%HqtDOdft$C7Gj1*y7(vfGEy^7cIxVN~xIgobMi=)Cx zL)!%(0K-^Oh0kOfI~$pn-eOS5Q&DC!^`ye{zAs<{gD;jD7%^e`!PsrUlqT{1E=qxy?qpBrzCcw#EyzvgeG z7!lEc-%a?_litGVf#=|Gjmp^X2XQuT(~_!60z`h8d(NM8v>r{X605R+nLX@eTTOo- z;$T^bqMj%ABMf30ZO>O2@ZN}LTWS3te)GyJ?$AW1%uu)vKsK!`V)OEZ;}Zxn-plgbH7R;bBP$W~|adpDY;ymJ(EerLQYNqyXBBu23bb>=2~sizU`1+zWn1HM3=2`s1`J`g0_c)Hjn;Ck#H(z z77!K&lPit=v1mLKHWQ4*iH7|PVOOrY3P1-5`?toD!hxEO@~{H54dWs_`a<9L_8%;K zkeHhU@)e2VP~`a__az!b6U%y43B8lyw%sCbKXY0x$)?5}Q+^wElj&H-+o!1ZJaZCoi_e~kT`k8OU=zh>Ksdj)pH>m9GeUi1`(eG9xM5z?J#oq(M9tGAW z9?Uw7xCpN>?y+C!(d)bX`QxwWGIl*7?(A*LZF#swLP@qC*pYaklW9XA*m|K z95Ac_T6LjNB`P#>huNmeXtr`}h+RDk)SkVFolwkInfC{=00)?wPQ-*6h(s#y~@!#Ukf2< zZk&U|fQIx#h#Jqktn^?2wiU>QZLMK@Fkc)Lu@9d~T0}wfpi0$;Vw3m0#=H3LXpg%K zwz5n1F~!rw_&=$x(Rj#b51`CB%e~?uZWkWRJ`mQ>D~l$$^A25vAtT&It0>2|70hB? zT~CF2=cqt=%T5gPEH-iziTTty^s`r4`_&1QE^VZ67sR&ul|Rz}AX@!L1J#m17$Fx= z>-?Q)l-nCcD}1=@e1=6<;P0+Do43R4)FBTieXjhI-541UdGmM0ue|2ij&ILA({$`6 zsoTIAAoqSFhj5t^xg#Mc*lP-J6oGQN64b#@x?8uJ7M6`D*5j7ZmIQCDw*}3DS~y)S z;r?W=KY480gD2wv7)Q-^s!0c8T}|b8Pq*biObK5Ju8u6hk%`Ju{B3~?B&8vG1%#3gGtg574FWw80UZRWKP zF%9sTI(`X)<1H}fkH`)oJn5eh?b}^oF|t>X4%gm=tprQ&0(?As4TQYuH+@)VtKWBe zBJa+TwAu%K*7%(=f6qeuK=iOE#Z1@kQAO6)JLpjj|t`| z)99J;zS^)REz)fP^JvI~G2^^yjucODh{(|irXw|}KGrF;vNjJ>O{>D|WCG=`QCvoC zhfYy5Vyyw|dgQXwKyp8&>2A#X+KTAX<^BY70@X6-sy_t6a*CCO>{(B{4 zz$itg`@F?k_F88xclpROVljEo>Byozqj%IQ#|^-*sNDwjAR9?w1{~+cw~L6xlcKIx z|50{*{pYOaI3+T5(vh(Ago1paMwWHbtxd9=y0I!MFZ8$HIC^`+==ma;_5gtgTe+Jb zRL0L}+skXj2LR(QAX(}tk{Pq0n%$^WH?1voBNeVzNO}rVzm0aMNnb^Cg7Z&Q=a=GzTK{4?>{m= z=`IfA=@E#t`Xu69m+wZ=lINS9n^NBk3@k{GJe5~fr9AAPcDXv9agP2x80B$gBRU;z za#)5fVRB4M`7rnK8)+rg$?0Khj76=yOPS?iQnM=b@C>`_zv>*@K|iN zj%@1X-12nf}m!((WGu$!@=vy-g@gQ1naqhp+^yB@wW&Tr|;0UNfd0GpUvYHB)i z>&X$}p_Tae;)=NaGggzyS+fHtAJylN0u~ksVc@8!XtMB0(S?Ld>jd_pLVU9@d+<&l z_|7}QZO?8;q}ZyKtPuc5DYq@REvI+heVz|L9g+Cq^jc6aw&;BT%P8%d{!|^`eU>qV za1fy~*KO1r`XcKf?S2s1B5e}5nIdDxXrvQe{0(a+C2a=ZO(V>_YEBYU-@D|n+kmab0K zx*ZqVErLzi@BO=Y15tUoAvgV|@;;QNsC<^CRoh8St3Vz2mbuI|@FA*$+f!qoQo&=Ab3;afUUJAX{Pi2GVi@K1H}5yWT#YsJ@kUy#i*%4 z-ZykB7yR;B-#P6#?dgN}qHv$7GzE4`Zx3gq)#7wMtKwRW7z=kF3#rkx6ctCN`}b_( zTup-Pt`^ji>gW%K@6L7RVRzug9?l(m*QRUg=_j#Sfx{a-9$_-MFCW8O+^kS#e!kM? z@wq9S`Bnh3+EGSD0bwrzHRZ|iO~EuFYYp^dKTYCj1C)bBB^k7_tGvYQ(F+QGpN<0A zy?87nzmB+OI;HW+$IlA0KbE4mfz!0~q)Bv*bH>=1$5VL^tmk4ZkmUB` z`AKL(Ie8(7xY2e(G-e!WpCN0ugntrci-aY2hT8$=YjG5>8e+~GqV<~R*odr`8x3xX zY|=TIGQP$-hNfzk(70yf^(&&S~?6VF~q^in@JbT zAE;ZmVzQVESNvTAc|FH)fl>1OJ5BhNvqO3Q?NL(o3XG_O}c;ehmlyp z9jgzZi%Z+ijRRPTs_0Qxj#1sn7KjxPd^?_KH*^y0A83UzCY-(OsC`qcYoJbIJrjJR-sDBTMe4?YHn0Wgc3qgSfBzRN0x_ zxX%ntzx$W$Abu;dJ(J_H3VVEvK4N1YsE>%hsyu5YZBOovMX+jWzqZAQzxzdm@~F#` zzv(`T-mX^{x?6LSgZ=pH-pgLIIyAhTfpK`ZOo|jIVe-aw_$L#sx-xohK+=jlNTT8w zfCL`z1;gq1NcHgq^Qncaf+5C{c!-`Zccy%t0uKw4h>xx6*`57V=qY$u1UbRCU51GA zv@9A2)&>#wnxGW82zo6?dTTh#LbvBpzZ&V|jc9S+(%@V2n1Qo;LbhvaLq?1|g!eOq zgc1kvo*Y0IX`s!ZWvMrU;+1*NuIeBJpv3{B4(@rV<@%t(09&T#2MuU$=MMA{xn9nl^<}8cHfj3@1cv{icR-d`o7p2A_8Ml(bnx1DjMR^pRBeBQ<|Y46#i97n#Ymrv{%@+u;L<}VT1OOdCGE}>IEV#XX;Rk+t~H0f=nb8V72uy zbOl>U_d7##K?2(@zjQ!x-a1#prm>(ELs1chc6ftaKZoXduad&t~wdE3rKgE2$MeD_=zSqqa` ztlG5DeRq@Xr+4ZaQ5QFo5?m~Tvszc*&32We5x~$TaZzjI`h4_1gnwYxyvbfF3-940 zi-c3GkgT2$3wOz-=k|>dUQ35O*+PGQD1|^4;;l7IREkxCodpt}kJE}p0zCGhCb@a8 z269|I_7uh2`UQUoi`lr03OY0_>8_sMB+T)(DHr$#*{6j><9_0l1*P512p`EbqC=(m z9%zebY*jmO(n8)f;#;`D7ORdL?%Fe=GRUs;cok{h&bWCnA9)DBB!IMaX* z{fhTxQ#C1eB~VbMc9FOj0YFz)4OIA6iasfV&BbG}q%~)>QxMqPfL}A{l18;PXk~<{ zjIJ#`L5bfxDR!tIKuRAhk7mvtzKDtIOlf0j?v3mm4PDP0W~@$Em!FAoF_7hhNn6D+ zp>An6tFmB8$!ST+ZK-eIB7s@_y+`qTk4TDrL{;sqx@F~DGE$~~Hef91{bu_FsonI0 z$;8F{lanFBNhhz`E?5PIB&UsUaVw|hP4 zuA&HKu9mN-JxS$WEhvxw_;T2GJ$!;9p}=QUMYh-~wt`SzAUsWlDVQzwt{09&Y(cNP z%ndHAnkA9El_%J>3t)%GH<@m+VX^~$aI-ooK2R^n%38DViL{{FiIw3TqI#kxEBu}} zg4Qfhe6Y_p`9VeI*2NP6d0sH{THT7nFV~UdyLyukrs?qJFph?^iasKO{I*NpUP!5e zvrs)vODHp^@EG&L_D=+fHT+Q%u$t>CRUkzuV}3IUf^2ah}lUCC)64 z>Qef_a0Tu~*8y4Q5(Xq2nykUAAHclq0q>4m0@_*yVG#T4BHT4UXBWwd_Hy;sFkl75 zK`1r~K+dSh>xV2T_1S`NV1jqCO&2=|w+xQ*?~~WKah6qTyAC%d^S2%^KUaRyg*9$3 zEPd{ji8~MAUT-WKPqJLTBsjTe=G1k&qZ!+%T;8y>{|*Qfj}ehffUV(Lgl7A-sBw^V z<9~}~w8q+fp%H)P+yEBqkjU7CtRsAd;pxKrUcq2Y$A9j^D#~O{OwNlFgzWi|eUqzv zfxhQcjzKCqbw|KHDb_W%Mq2`4+ z_jiTNP4a+pwFL||tE-uZZyC+2>{A{P#-%L9Cqlvj_bt{U>f?3Em7kjYV%4P8Nt%-g zmpjAG*(ipOm)pAh4Gl-*dsFEn%IKLr(PZC!qovlL7df28_Ygd}p4MaE%w(=ZiM&5L zaQcxChJSKHycjjLvVNdqmVLJN%>VBm8TGY>BLFw2xbkId{P|^TO!LpyxC)Yg3hwp4 z>4i}gw~Uu^i)uvRHwfGq2_%53r7PYH#>yP7dMJ-LJ}9VsFa#4Z?Ysm$ESbz_G;Uvk z4E!^-py}@->I= z;Ay<<10SBQzIgkmR!a<5mR)AVF}erMQ#%=JF{Y1tij1;NiSlO4F|}NYjo-|rIQV3J z7(T0?XfnUFGglxXgs4KGOef6y0^AKy0W8zF*vRdfVH%H9{|d4Ogsrd*RXh^FMN%d( zYb6GlWG`wkddP>#yAmjk|60MXYGpR$+>tYP7{0o}GgJt66B%?EKcmA@jY}?30EPRN zvBl(PjUGn9I!9NLI2xfU$)x%uClk7C;7p@Mh|T=q;xW$N05^wr zDc;3x;PK_&q3@$3pa=A}z_3X~Mj!$;=@gTBC)OOuC^W0HOVQ;AhdJi#9XRt&)vZ}h zbGtE!dI|IMDhZVf(NHqvfaM22ESTEbG6|nkycDJ?RP%-MI9K?#pA(0pJy6c}FJD#z zmr{MQw=9ZaIqLKJbw7dQ%ZF8rF5jMFcD;S01JA@85CiOq{=K9`v}b7j=>v zsz>^E#@)Gh~ZI8Uw@lg!5J2@Q|Y7R z!0OO-dc@dGY5*SO1dhm*_HN~m&>&7rsD2qjShm0bPo@3pxT`i%BiY(8 zch;AL{XT^?jK`GyEN4XP0(Q~pWfNjC0-H2^PmD({c6l`C_pwER*@jPdpx5t6x$aS6 zK~ifR2cT%!Bz1r>a$s*q=^;9v?X5UDHyljb4bL_PNO9dL3P3}9IlK?Tr}(0 z)%`vDnE;KpzxUAZpcN+f^UPL*%%FUThg?4?^4##afpk*)ymx#?%#C^`RzU5 zyRfvd6ox4@znZAQ4<6U(4n<#hf)k(sYYnAAgOVn|5oh*Y!14f|lxU(7lAB7ClPKH^ zIv1d`zZPPfMRdWjDgBw&!}c5{k9=Bl7nwKq7dE{y9e412CX3lS5f7Z9En2EUHTR%M z3=2WylH}gxYvb>`ujYDd@qq&Z@!=qswK|y0JqJ-Qq=OR;oo#rdIecYqlg8JI~dsbb=I5Q;$tbqPYwi`2c z5bl!-0kVmz_H%tZUSeCR26;7G7`kJ#4_W5-vhvSGT%j|uEn+c(n*CYu4UUN|(EX_Z zG@}vmiyc9g8=lhNU%ub5yKK;26>~r-L9ZcM+9xy;_*;P87Hstw0OzxHI-)$j^PuG& zZT-kVJ6aLX3*vC(JuzSmXBy&z5IdZUy?>LTmlx(vinV3sN**_VR!w4K4=RS(YO#m?!EM%YDK` zRBnyfT#^F0bByN3jcfY>`tN)0^)**TD+l$b_KHO2Y%AfjL;4Y zOV@0J{jYjQmlx9NxoLq>5D*YtAcZC|AacZ}ix@fJv{?cJqPAnBoxDy$nc=HPDW)2? z5ENaz3Q=2*_|478(NW~WY#LxI9;fXEemNKr@%KYq6R=VlY zrFzf&85(9Ialj|epAeAFqJ8XbVAA*h>v-*-nF5KlXd(U)s${f+!Coccg3jI>)T8_S z237SL?ER6djAKCGdc{o%VcsN5TSD+RDZ+v4k2K^w3HgR)dfxvTk>))O{f4;&j{m_V zf{Xqz-+j$TMf}GVT_nEA?3XH0YG`0)Iwg>`AGK;Hwe*ixLi*Yt$s?-<;*E_ZAMB4+ z&Z5&ly>z8oe=LNTefpy}UODy$%dS@ZqxY*e1|0SutII_lu%f`YMn1v+N+jkw<`44! zpG4aJD}6x0(82#xR0~9JV)?hK7AV`q^;R1j(Zuj3v^BB533pArZvtnt^qUacZ1W~O zHcPz;QZ1@)LVk-j!e3>yK$KP);(ri%Zvyje2oMmQ7Z;db68C?u>M!@mz%xD!;5{+{ z@U8{^ji;j(7mN<`6{o|52R^s{!4cbV!P>B2asD=Ty|4Q`N>Fr-cz8tU0GKkPX!FJp?5B1w|!IA`D za5`XN`y0W3HnJcfq<;|rW&U`A1ib&FY(e}|(FqCyq5=*A!ts}~qw=eA8#V$^Qw|%b z+wsS3+YVeXZ`Ienj1JsamtU8bm%*|xzxRKU%;*ABG{}L!JK%}`Nu7U}ix-P8JMe$6 z>My2xK-x}sYrX@|J8{9tjer%Y?}1ERXs-hmKEo0eybQd3+3NHBH4w%4RWYcG@@;TM7cLl@ z*(-L|MfHZ^cjJQnwg7e*vj87^;NLLM7c9=|Rj;v|>eUFoC1%CQi;<-lBkX?}sdM^6 z2nD3=!G9x=?7;lC)K{0o1X!)SUT({~mqg(D%UWkB zFd&czNZ9vhz6^c1VDaIv)~x$Jybcb`t;RA%1p(=$`EM&ikuQp*!1ca2EB{%sKtNdj zvT~aFqR2`oC;6ZC;h&5VoiI6yd5L7|OCbIpu$A*?!tlVX{x^0vicKZGmxmko9S8{b zU+lg4FG+y|Jnm)Wn>ps3AKM_V8!Ua2R z09I6g{O3Ntk=wjrBF!)4R6wRWjQ>Oax+uSp3cvh_{vuy)1Ew{S0MUn$q4e7S>G^*! CY2(!Z delta 11130 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

    G(;4eI7C?++(cQMC>R}|&hR>Z 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d94401f1e..ed0a26aa4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jul 28 11:16:06 BST 2015 +#Wed Sep 23 15:50:30 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip diff --git a/gradlew b/gradlew index 91a7e269e..97fac783e 100755 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs index 83412a90a..30f00a942 100644 --- a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs +++ b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs @@ -8,6 +8,7 @@ org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.source=1.7 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 diff --git a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs index 6518db6af..1f4325cd5 100644 --- a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs +++ b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs @@ -13,7 +13,7 @@ cleanup.always_use_this_for_non_static_field_access=true cleanup.always_use_this_for_non_static_method_access=false cleanup.convert_functional_interfaces=false cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false +cleanup.correct_indentation=true cleanup.format_source_code=true cleanup.format_source_code_changes_only=false cleanup.insert_inferred_type_arguments=false diff --git a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs index 6518db6af..1f4325cd5 100644 --- a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs +++ b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs @@ -13,7 +13,7 @@ cleanup.always_use_this_for_non_static_field_access=true cleanup.always_use_this_for_non_static_method_access=false cleanup.convert_functional_interfaces=false cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false +cleanup.correct_indentation=true cleanup.format_source_code=true cleanup.format_source_code_changes_only=false cleanup.insert_inferred_type_arguments=false diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs index dc84ae74c..c5e23c905 100644 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs @@ -50,21 +50,27 @@ cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access=false cleanup.use_this_for_non_static_field_access_only_if_necessary=false cleanup.use_this_for_non_static_method_access=false cleanup.use_this_for_non_static_method_access_only_if_necessary=true cleanup.use_type_arguments=false -cleanup_profile=_Spring Rest Docs Cleanup Conventions +cleanup_profile=_Spring REST Docs Cleanup Conventions cleanup_settings_version=2 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring Rest Docs Java Conventions +formatter_profile=_Spring REST Docs Java Conventions formatter_settings_version=12 org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=true -org.eclipse.jdt.ui.keywordthis=true +org.eclipse.jdt.ui.gettersetter.use.is=false +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; +org.eclipse.jdt.ui.javadoc=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true +org.eclipse.jdt.ui.staticondemandthreshold=9999 +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true @@ -75,22 +81,20 @@ sp_cleanup.add_missing_override_annotations=true sp_cleanup.add_missing_override_annotations_interface_methods=true sp_cleanup.add_serial_version_id=false sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_parentheses_in_expressions=true sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=true -sp_cleanup.convert_functional_interfaces=false +sp_cleanup.always_use_this_for_non_static_method_access=false sp_cleanup.convert_to_enhanced_for_loop=false sp_cleanup.correct_indentation=false sp_cleanup.format_source_code=true sp_cleanup.format_source_code_changes_only=false -sp_cleanup.insert_inferred_type_arguments=false -sp_cleanup.make_local_variable_final=true +sp_cleanup.make_local_variable_final=false sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=true +sp_cleanup.make_private_fields_final=false sp_cleanup.make_type_abstract_if_missing_method=false sp_cleanup.make_variable_declarations_final=false sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.never_use_parentheses_in_expressions=false sp_cleanup.on_save_use_additional_actions=true sp_cleanup.organize_imports=true sp_cleanup.qualify_static_field_accesses_with_declaring_class=false @@ -99,13 +103,12 @@ sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class= sp_cleanup.qualify_static_member_accesses_with_declaring_class=true sp_cleanup.qualify_static_method_accesses_with_declaring_class=false sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_redundant_type_arguments=true -sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces=true sp_cleanup.remove_trailing_whitespaces_all=true sp_cleanup.remove_trailing_whitespaces_ignore_empty=false sp_cleanup.remove_unnecessary_casts=true sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_imports=true sp_cleanup.remove_unused_local_variables=false sp_cleanup.remove_unused_private_fields=true sp_cleanup.remove_unused_private_members=false @@ -113,13 +116,10 @@ sp_cleanup.remove_unused_private_methods=true sp_cleanup.remove_unused_private_types=true sp_cleanup.sort_members=false sp_cleanup.sort_members_all=false -sp_cleanup.use_anonymous_class_creation=false -sp_cleanup.use_blocks=false +sp_cleanup.use_blocks=true sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_lambda=true sp_cleanup.use_parentheses_in_expressions=false sp_cleanup.use_this_for_non_static_field_access=true sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=false -sp_cleanup.use_type_arguments=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java index fc72501e6..401b67139 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -37,7 +37,7 @@ public class RestDocumentation implements TestRule { /** * Creates a new {@code RestDocumentation} instance that will generate snippets to the - * given {@code outputDirectory} + * given {@code outputDirectory}. * * @param outputDirectory the output directory */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java index cad0c71ed..86dbf6c8e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java @@ -39,7 +39,7 @@ public final class RestDocumentationContext { * Creates a new {@code RestDocumentationContext} for a test on the given * {@code testClass} with given {@code testMethodName} that will generate * documentation to the given {@code outputDirectory}. - * + * * @param testClass the class whose test is being executed * @param testMethodName the name of the test method that is being executed * @param outputDirectory the directory to which documentation should be written. @@ -52,7 +52,7 @@ public RestDocumentationContext(Class testClass, String testMethodName, } /** - * Returns the class whose tests are currently executing + * Returns the class whose tests are currently executing. * * @return The test class */ @@ -61,8 +61,8 @@ public Class getTestClass() { } /** - * Returns the name of the test method that is currently executing - * + * Returns the name of the test method that is currently executing. + * * @return The name of the test method */ public String getTestMethodName() { @@ -70,7 +70,7 @@ public String getTestMethodName() { } /** - * Returns the current step count and then increments it + * Returns the current step count and then increments it. * * @return The step count prior to it being incremented */ @@ -79,7 +79,7 @@ int getAndIncrementStepCount() { } /** - * Returns the current step count + * Returns the current step count. * * @return The current step count */ @@ -89,7 +89,7 @@ public int getStepCount() { /** * Returns the output directory to which generated snippets should be written. - * + * * @return the output directory */ public File getOutputDirectory() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java index 1adf117ef..31a3fd51f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java @@ -19,8 +19,8 @@ import java.util.Map; /** - * A constraint - * + * A constraint. + * * @author Andy Wilkinson */ public class Constraint { @@ -32,7 +32,7 @@ public class Constraint { /** * Creates a new {@code Constraint} with the given {@code name} and * {@code configuration}. - * + * * @param name the name * @param configuration the configuration */ @@ -42,8 +42,8 @@ public Constraint(String name, Map configuration) { } /** - * Returns the name of the constraint - * + * Returns the name of the constraint. + * * @return the name */ public String getName() { @@ -51,8 +51,8 @@ public String getName() { } /** - * Returns the configuration of the constraint - * + * Returns the configuration of the constraint. + * * @return the configuration */ public Map getConfiguration() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java index 7a428e3f6..2bd63472f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java @@ -18,7 +18,7 @@ /** * Resolves a description for a {@link Constraint}. - * + * * @author Andy Wilkinson * */ @@ -26,9 +26,9 @@ public interface ConstraintDescriptionResolver { /** * Resolves the description for the given {@code constraint}. - * + * * @param constraint the constraint * @return the description */ - public String resolveDescription(Constraint constraint); + String resolveDescription(Constraint constraint); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java index 23d55d8c6..7fc11026d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java @@ -21,8 +21,8 @@ import java.util.List; /** - * Provides access to descriptions of a class's constraints - * + * Provides access to descriptions of a class's constraints. + * * @author Andy Wilkinson */ public class ConstraintDescriptions { @@ -38,7 +38,7 @@ public class ConstraintDescriptions { * Constraints will be resolved using a {@link ValidatorConstraintResolver} and * descriptions will be resolved using a * {@link ResourceBundleConstraintDescriptionResolver}. - * + * * @param clazz the class */ public ConstraintDescriptions(Class clazz) { @@ -51,7 +51,7 @@ public ConstraintDescriptions(Class clazz) { * Constraints will be resolved using the given {@code constraintResolver} and * descriptions will be resolved using a * {@link ResourceBundleConstraintDescriptionResolver}. - * + * * @param clazz the class * @param constraintResolver the constraint resolver */ @@ -63,7 +63,7 @@ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResol * Create a new {@code ConstraintDescriptions} for the given {@code clazz}. * Constraints will be resolved using a {@link ValidatorConstraintResolver} and * descriptions will be resolved using the given {@code descriptionResolver}. - * + * * @param clazz the class * @param descriptionResolver the description resolver */ @@ -76,7 +76,7 @@ public ConstraintDescriptions(Class clazz, * Create a new {@code ConstraintDescriptions} for the given {@code clazz}. * Constraints will be resolved using the given {@code constraintResolver} and * descriptions will be resolved using the given {@code descriptionResolver}. - * + * * @param clazz the class * @param constraintResolver the constraint resolver * @param descriptionResolver the description resolver @@ -89,8 +89,8 @@ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResol } /** - * Returns a list of the descriptions for the constraints on the given property - * + * Returns a list of the descriptions for the constraints on the given property. + * * @param property the property * @return the list of constraint descriptions */ @@ -104,5 +104,4 @@ public List descriptionsForProperty(String property) { Collections.sort(descriptions); return descriptions; } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java index 8f5c6580e..ed5ce37aa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java @@ -20,7 +20,7 @@ /** * An abstraction for resolving a class's constraints. - * + * * @author Andy Wilkinson */ public interface ConstraintResolver { @@ -28,7 +28,7 @@ public interface ConstraintResolver { /** * Resolves and returns the constraints for the given {@code property} on the given * {@code clazz}. If there are no constraints, an empty list is returned. - * + * * @param property the property * @param clazz the class * @return the list of constraints, never {@code null} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 7ea94a8d6..660ed4b5f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -45,7 +45,7 @@ * {@code javax.validation.constraints.NotNull.description}. *

    * Default descriptions are provided for Bean Validation 1.1's constraints: - * + * *

      *
    • {@link AssertFalse} *
    • {@link AssertTrue} @@ -61,7 +61,7 @@ *
    • {@link Pattern} *
    • {@link Size} *
    - * + * * @author Andy Wilkinson */ public class ResourceBundleConstraintDescriptionResolver implements @@ -78,7 +78,7 @@ public class ResourceBundleConstraintDescriptionResolver implements * Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve * descriptions by looking them up in a resource bundle with the base name * {@code org.springframework.restdocs.constraints.ConstraintDescriptions} in the - * default locale loaded using the thread context class loader + * default locale loaded using the thread context class loader. */ public ResourceBundleConstraintDescriptionResolver() { this(getBundle("ConstraintDescriptions")); @@ -87,7 +87,7 @@ public ResourceBundleConstraintDescriptionResolver() { /** * Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve * descriptions by looking them up in the given {@code resourceBundle}. - * + * * @param resourceBundle the resource bundle */ public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) { @@ -126,7 +126,8 @@ private String getDescription(String key) { return this.defaultDescriptions.getString(key); } - private static class ConstraintPlaceholderResolver implements PlaceholderResolver { + private static final class ConstraintPlaceholderResolver implements + PlaceholderResolver { private final Constraint constraint; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index 041f2bd3d..664691c65 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -32,7 +32,7 @@ * constraints. The name of the constraint is the fully-qualified class name of the * constraint annotation. For example, a {@link NotNull} constraint will be named * {@code javax.validation.constraints.NotNull}. - * + * * @author Andy Wilkinson * */ @@ -43,7 +43,7 @@ public class ValidatorConstraintResolver implements ConstraintResolver { /** * Creates a new {@code ValidatorConstraintResolver} that will use a {@link Validator} * in its default configuration to resolve constraints. - * + * * @see Validation#buildDefaultValidatorFactory() * @see ValidatorFactory#getValidator() */ @@ -54,7 +54,7 @@ public ValidatorConstraintResolver() { /** * Creates a new {@code ValidatorConstraintResolver} that will use the given * {@code Validator} to resolve constraints. - * + * * @param validator the validator */ public ValidatorConstraintResolver(Validator validator) { @@ -78,5 +78,4 @@ public List resolveForProperty(String property, Class clazz) { } return constraints; } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java index 1692913c2..9fc48616d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java @@ -17,4 +17,5 @@ /** * Documenting a RESTful API's constraints. */ -package org.springframework.restdocs.constraints; \ No newline at end of file +package org.springframework.restdocs.constraints; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 0802bdf41..9270c126f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -53,7 +53,7 @@ protected CurlRequestSnippet() { /** * Creates a new {@code CurlRequestSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * + * * @param attributes The additional attributes */ protected CurlRequestSnippet(Map attributes) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java index 124090985..85ee026af 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -17,4 +17,5 @@ /** * Documenting the curl command required to make a request to a RESTful API. */ -package org.springframework.restdocs.curl; \ No newline at end of file +package org.springframework.restdocs.curl; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index c097e40e3..9aae38b71 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -22,7 +22,7 @@ /** * Static factory methods for documenting a RESTful API's HTTP requests. - * + * * @author Andy Wilkinson * @author Jonathan Pearlin */ @@ -35,7 +35,7 @@ private HttpDocumentation() { /** * Returns a handler that will produce a snippet containing the HTTP request for the * API call. - * + * * @return the handler that will produce the snippet */ public static Snippet httpRequest() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 322474d60..23bba3a9b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -55,7 +55,7 @@ protected HttpRequestSnippet() { /** * Creates a new {@code HttpRequestSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * + * * @param attributes The additional attributes */ protected HttpRequestSnippet(Map attributes) { @@ -186,4 +186,4 @@ private Map header(String name, String value) { return header; } -} \ No newline at end of file +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index b3483a548..e8f659503 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -47,7 +47,7 @@ protected HttpResponseSnippet() { /** * Creates a new {@code HttpResponseSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * + * * @param attributes The additional attributes */ protected HttpResponseSnippet(Map attributes) { @@ -86,4 +86,5 @@ private Map header(String name, String value) { header.put("value", value); return header; } -} \ No newline at end of file + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java index 258da97d6..5d3206012 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java @@ -18,4 +18,5 @@ * Documenting the HTTP request sent to a RESTful API and the HTTP response that is * returned. */ -package org.springframework.restdocs.http; \ No newline at end of file +package org.springframework.restdocs.http; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index ed877ab70..4b952ca2b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -25,8 +25,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** - * Abstract base class for a {@link LinkExtractor} that extracts links from JSON - * + * Abstract base class for a {@link LinkExtractor} that extracts links from JSON. + * * @author Andy Wilkinson */ abstract class AbstractJsonLinkExtractor implements LinkExtractor { @@ -43,4 +43,5 @@ public Map> extractLinks(OperationResponse response) } protected abstract Map> extractLinks(Map json); -} \ No newline at end of file + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java index e7bc955f6..ad12497a3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -25,7 +25,7 @@ /** * {@link LinkExtractor} that extracts links in Atom format. - * + * * @author Andy Wilkinson */ @SuppressWarnings("unchecked") @@ -62,4 +62,5 @@ private static void maybeStoreLink(Link link, extractedLinks.add(link.getRel(), link); } } -} \ No newline at end of file + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index 7bc57fffa..c3f28d733 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -28,7 +28,7 @@ /** * {@link LinkExtractor} that extracts links in Hypermedia Application Language (HAL) * format. - * + * * @author Andy Wilkinson */ class HalLinkExtractor extends AbstractJsonLinkExtractor { @@ -77,4 +77,5 @@ private static void maybeAddLink(Link possibleLink, List links) { links.add(possibleLink); } } -} \ No newline at end of file + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 968f5dad4..b167225e7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -23,7 +23,7 @@ /** * Static factory methods for documenting a RESTful API that utilizes Hypermedia. - * + * * @author Andy Wilkinson */ public abstract class HypermediaDocumentation { @@ -34,7 +34,7 @@ private HypermediaDocumentation() { /** * Creates a {@code LinkDescriptor} that describes a link with the given {@code rel}. - * + * * @param rel The rel of the link * @return a {@code LinkDescriptor} ready for further configuration */ @@ -46,7 +46,7 @@ public static LinkDescriptor linkWithRel(String rel) { * Returns a handler that will produce a snippet documenting the links in the API * call's response. Links will be extracted from the response automatically based on * its content type. - * + * * @param descriptors The descriptions of the response's links * @return the handler */ @@ -60,22 +60,22 @@ public static Snippet links(LinkDescriptor... descriptors) { * call's response. The given {@code attributes} will be available during snippet * generation. Links will be extracted from the response automatically based on its * content type. - * + * * @param attributes Attributes made available during rendering of the links snippet * @param descriptors The descriptions of the response's links * @return the handler */ public static Snippet links(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors), - attributes); + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), attributes); } /** * Returns a handler that will produce a snippet documenting the links in the API * call's response. Links will be extracted from the response using the given * {@code linkExtractor}. - * + * * @param linkExtractor Used to extract the links from the response * @param descriptors The descriptions of the response's links * @return the handler @@ -90,7 +90,7 @@ public static Snippet links(LinkExtractor linkExtractor, * call's response. The given {@code attributes} will be available during snippet * generation. Links will be extracted from the response using the given * {@code linkExtractor}. - * + * * @param attributes Attributes made available during rendering of the links snippet * @param linkExtractor Used to extract the links from the response * @param descriptors The descriptions of the response's links @@ -98,8 +98,7 @@ public static Snippet links(LinkExtractor linkExtractor, */ public static Snippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), - attributes); + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 694eec9e7..eed9dfb4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -19,7 +19,7 @@ import org.springframework.core.style.ToStringCreator; /** - * Representation of a link used in a Hypermedia-based API + * Representation of a link used in a Hypermedia-based API. * * @author Andy Wilkinson */ @@ -30,7 +30,7 @@ public class Link { private final String href; /** - * Creates a new {@code Link} with the given {@code rel} and {@code href} + * Creates a new {@code Link} with the given {@code rel} and {@code href}. * * @param rel The link's rel * @param href The link's href @@ -41,7 +41,7 @@ public Link(String rel, String href) { } /** - * Returns the link's {@code rel} + * Returns the link's {@code rel}. * @return the link's {@code rel} */ public String getRel() { @@ -49,7 +49,7 @@ public String getRel() { } /** - * Returns the link's {@code href} + * Returns the link's {@code href}. * @return the link's {@code href} */ public String getHref() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index cb7e9909a..810a52448 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -19,11 +19,10 @@ import org.springframework.restdocs.snippet.AbstractDescriptor; /** - * A description of a link found in a hypermedia API - * - * @see HypermediaDocumentation#linkWithRel(String) - * + * A description of a link found in a hypermedia API. + * * @author Andy Wilkinson + * @see HypermediaDocumentation#linkWithRel(String) */ public class LinkDescriptor extends AbstractDescriptor { @@ -33,7 +32,7 @@ public class LinkDescriptor extends AbstractDescriptor { /** * Creates a new {@code LinkDescriptor} describing a link with the given {@code rel}. - * + * * @param rel the rel of the link */ protected LinkDescriptor(String rel) { @@ -41,7 +40,7 @@ protected LinkDescriptor(String rel) { } /** - * Marks the link as optional + * Marks the link as optional. * * @return {@code this} */ @@ -51,8 +50,8 @@ public final LinkDescriptor optional() { } /** - * Returns the rel of the link described by this descriptor - * + * Returns the rel of the link described by this descriptor. + * * @return the rel */ public final String getRel() { @@ -60,8 +59,8 @@ public final String getRel() { } /** - * Returns {@code true} if the described link is optional, otherwise {@code false} - * + * Returns {@code true} if the described link is optional, otherwise {@code false}. + * * @return {@code true} if the described link is optional, otherwise {@code false} */ public final boolean isOptional() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index d6b225e05..8b5a04d46 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -52,7 +52,7 @@ public class LinksSnippet extends TemplatedSnippet { /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. - * + * * @param linkExtractor the link extractor * @param descriptors the link descriptors */ @@ -64,7 +64,7 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. The * given {@code attributes} will be included in the model during template rendering. - * + * * @param linkExtractor the link extractor * @param descriptors the link descriptors * @param attributes the additional attributes @@ -139,7 +139,7 @@ private List> createLinksModel() { /** * Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their * {@link LinkDescriptor#getRel() rels}. - * + * * @return the link descriptors */ protected final Map getDescriptorsByRel() { @@ -147,8 +147,8 @@ protected final Map getDescriptorsByRel() { } /** - * Returns a model for the given {@code descriptor} - * + * Returns a model for the given {@code descriptor}. + * * @param descriptor the descriptor * @return the model */ @@ -161,4 +161,4 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor return model; } -} \ No newline at end of file +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java index a792c083c..c8d3dab75 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java @@ -17,4 +17,5 @@ /** * Documenting a RESTful API that uses hypermedia. */ -package org.springframework.restdocs.hypermedia; \ No newline at end of file +package org.springframework.restdocs.hypermedia; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java index fdd5fe116..a00533639 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java @@ -20,35 +20,35 @@ /** * Describes an operation performed on a RESTful service. - * + * * @author Andy Wilkinson */ public interface Operation { /** * Returns a {@code Map} of attributes associated with the operation. - * + * * @return the attributes */ Map getAttributes(); /** * Returns the name of the operation. - * + * * @return the name */ String getName(); /** * Returns the request that was sent. - * + * * @return the request */ OperationRequest getRequest(); /** * Returns the response that was received. - * + * * @return the response */ OperationResponse getResponse(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 919e84adb..052093ce4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -24,7 +24,7 @@ /** * The request that was sent as part of performing an operation on a RESTful service. - * + * * @author Andy Wilkinson * @see Operation#getRequest() */ @@ -32,22 +32,22 @@ public interface OperationRequest { /** * Returns the contents of the request. If the request has no content an empty array - * is returned - * + * is returned. + * * @return the contents, never {@code null} */ byte[] getContent(); /** * Returns the headers that were included in the request. - * + * * @return the headers */ HttpHeaders getHeaders(); /** - * Returns the HTTP method of the request - * + * Returns the HTTP method of the request. + * * @return the HTTP method */ HttpMethod getMethod(); @@ -56,7 +56,7 @@ public interface OperationRequest { * Returns the request's parameters. For a {@code GET} request, the parameters are * derived from the query string. For a {@code POST} request, the parameters are * derived form the request's body. - * + * * @return the parameters */ Parameters getParameters(); @@ -64,14 +64,14 @@ public interface OperationRequest { /** * Returns the request's parts, provided that it is a multipart request. If not, then * an empty {@link Collection} is returned. - * + * * @return the parts */ Collection getParts(); /** * Returns the request's URI. - * + * * @return the URI */ URI getUri(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java index 09e9bb80f..69a6feca3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java @@ -19,37 +19,37 @@ import org.springframework.http.HttpHeaders; /** - * A part of a multipart request - * - * @author awilkinson + * A part of a multipart request. + * + * @author Andy Wilkinson * @see OperationRequest#getParts() */ public interface OperationRequestPart { /** * Returns the name of the part. - * + * * @return the name */ String getName(); /** * Returns the name of the file that is being uploaded in this part. - * + * * @return the name of the file */ String getSubmittedFileName(); /** * Returns the contents of the part. - * + * * @return the contents */ byte[] getContent(); /** * Returns the part's headers. - * + * * @return the headers */ HttpHeaders getHeaders(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index d72544773..18485fdb2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -21,7 +21,7 @@ /** * The response that was received as part of performing an operation on a RESTful service. - * + * * @author Andy Wilkinson * @see Operation * @see Operation#getRequest() @@ -30,14 +30,14 @@ public interface OperationResponse { /** * Returns the status of the response. - * + * * @return the status */ HttpStatus getStatus(); /** * Returns the headers in the response. - * + * * @return the headers */ HttpHeaders getHeaders(); @@ -45,7 +45,7 @@ public interface OperationResponse { /** * Returns the contents of the response. If the response has no content an empty array * is returned. - * + * * @return the contents, never {@code null} */ byte[] getContent(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index d53392d1d..9660a9816 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -24,8 +24,8 @@ import org.springframework.util.LinkedMultiValueMap; /** - * The parameters received in a request - * + * The parameters received in a request. + * * @author Andy Wilkinson */ @SuppressWarnings("serial") @@ -33,8 +33,8 @@ public class Parameters extends LinkedMultiValueMap { /** * Converts the parameters to a query string suitable for use in a URI or the body of - * a form-encoded request - * + * a form-encoded request. + * * @return the query string */ public String toQueryString() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java index a748df456..736fc4b5a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java @@ -19,8 +19,8 @@ import java.util.Map; /** - * Standard implementation of {@link Operation} - * + * Standard implementation of {@link Operation}. + * * @author Andy Wilkinson */ public class StandardOperation implements Operation { @@ -34,8 +34,8 @@ public class StandardOperation implements Operation { private final Map attributes; /** - * Creates a new {@code StandardOperation} - * + * Creates a new {@code StandardOperation}. + * * @param name the name of the operation * @param request the request that was sent * @param response the response that was received diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index b4bac39c9..9dfac8c03 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -26,7 +26,7 @@ /** * Standard implementation of {@link OperationRequest}. - * + * * @author Andy Wilkinson */ public class StandardOperationRequest implements OperationRequest { @@ -46,7 +46,7 @@ public class StandardOperationRequest implements OperationRequest { /** * Creates a new request with the given {@code uri} and {@code method}. The request * will have the given {@code headers}, {@code parameters}, and {@code parts}. - * + * * @param uri the uri * @param method the method * @param content the content diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index cfa93d117..b6c7bd034 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -20,7 +20,7 @@ /** * Standard implementation of {@code OperationRequestPart}. - * + * * @author Andy Wilkinson */ public class StandardOperationRequestPart implements OperationRequestPart { @@ -35,7 +35,7 @@ public class StandardOperationRequestPart implements OperationRequestPart { /** * Creates a new {@code StandardOperationRequestPart} with the given {@code name}. - * + * * @param name the name of the part * @param submittedFileName the name of the file being uploaded by this part * @param content the contents of the part diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index c8fcf401e..faac5f5a9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -21,7 +21,7 @@ /** * Standard implementation of {@link OperationResponse}. - * + * * @author Andy Wilkinson */ public class StandardOperationResponse implements OperationResponse { @@ -35,7 +35,7 @@ public class StandardOperationResponse implements OperationResponse { /** * Creates a new response with the given {@code status}, {@code headers}, and * {@code content}. - * + * * @param status the status of the response * @param headers the headers of the response * @param content the content of the response diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java index 2321f69e1..d7786bca2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java @@ -18,4 +18,5 @@ * Operation API that describes a request that was sent and the response that was * received when calling a RESTful API. */ -package org.springframework.restdocs.operation; \ No newline at end of file +package org.springframework.restdocs.operation; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java index 54fb82795..642c00278 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java @@ -23,15 +23,15 @@ * A {@code ContentModifier} modifies the content of an {@link OperationRequest} or * {@link OperationResponse} during the preprocessing that is performed prior to * documentation generation. - * + * * @author Andy Wilkinson * @see ContentModifyingOperationPreprocessor */ public interface ContentModifier { /** - * Returns modified content based on the given {@code originalContent} - * + * Returns modified content based on the given {@code originalContent}. + * * @param originalContent the original content * @return the modified content */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 0531c46d5..18f5602ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -25,7 +25,7 @@ /** * An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content * of the request or response. - * + * * @author Andy Wilkinson */ public class ContentModifyingOperationPreprocessor implements OperationPreprocessor { @@ -35,7 +35,7 @@ public class ContentModifyingOperationPreprocessor implements OperationPreproces /** * Create a new {@code ContentModifyingOperationPreprocessor} that will apply the * given {@code contentModifier} to the operation's request or response. - * + * * @param contentModifier the contentModifier */ public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java index ee57e83a4..76490760a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java @@ -25,7 +25,7 @@ * An {@link OperationRequestPreprocessor} that delgates to one or more * {@link OperationPreprocessor OperationPreprocessors} to preprocess an * {@link OperationRequest}. - * + * * @author Andy Wilkinson * */ @@ -37,7 +37,7 @@ class DelegatingOperationRequestPreprocessor implements OperationRequestPreproce * Creates a new {@code DelegatingOperationRequestPreprocessor} that will delegate to * the given {@code delegates} by calling * {@link OperationPreprocessor#preprocess(OperationRequest)}. - * + * * @param delegates the delegates */ DelegatingOperationRequestPreprocessor(List delegates) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java index e200c25d6..f55ce4eb1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java @@ -25,7 +25,7 @@ * An {@link OperationResponsePreprocessor} that delgates to one or more * {@link OperationPreprocessor OperationPreprocessors} to preprocess an * {@link OperationResponse}. - * + * * @author Andy Wilkinson */ class DelegatingOperationResponsePreprocessor implements OperationResponsePreprocessor { @@ -36,7 +36,7 @@ class DelegatingOperationResponsePreprocessor implements OperationResponsePrepro * Creates a new {@code DelegatingOperationResponsePreprocessor} that will delegate to * the given {@code delegates} by calling * {@link OperationPreprocessor#preprocess(OperationResponse)}. - * + * * @param delegates the delegates */ DelegatingOperationResponsePreprocessor(List delegates) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index ef258a15e..36eba207c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -27,8 +27,8 @@ import org.springframework.restdocs.operation.StandardOperationResponse; /** - * An {@link OperationPreprocessor} that removes headers - * + * An {@link OperationPreprocessor} that removes headers. + * * @author Andy Wilkinson */ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 74a5ff5f9..1a2b56cd3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -19,8 +19,8 @@ import java.util.regex.Pattern; /** - * A content modifier the masks the {@code href} of any hypermedia links - * + * A content modifier the masks the {@code href} of any hypermedia links. + * * @author Andy Wilkinson */ class LinkMaskingContentModifier implements ContentModifier { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java index e28bcaddf..bfdfbc25e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java @@ -23,22 +23,22 @@ /** * An {@code OperationPreprocessor} processes the {@link OperationRequest} and * {@link OperationResponse} of an {@link Operation} prior to it being documented. - * + * * @author Andy Wilkinson */ public interface OperationPreprocessor { /** - * Processes the given {@code request} - * + * Processes the given {@code request}. + * * @param request the request to process * @return the processed request */ OperationRequest preprocess(OperationRequest request); /** - * Processes the given {@code response} - * + * Processes the given {@code response}. + * * @param response the response to process * @return the processed response */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java index 39fd21329..24cf7535c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java @@ -21,7 +21,7 @@ /** * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} * prior to it being documented. - * + * * @author Andy Wilkinson */ public interface OperationRequestPreprocessor { @@ -29,7 +29,7 @@ public interface OperationRequestPreprocessor { /** * Processes and potentially modifies the given {@code request} before it is * documented. - * + * * @param request the request * @return the modified request */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 0b702f912..81464c91b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -22,7 +22,7 @@ /** * A {@link ContentModifier} that modifies the content by replacing occurrences of a * regular expression {@link Pattern}. - * + * * @author Andy Wilkinson * @author Dewet Diener */ @@ -35,7 +35,7 @@ class PatternReplacingContentModifier implements ContentModifier { /** * Creates a new {@link PatternReplacingContentModifier} that will replace occurences * the given {@code pattern} with the given {@code replacement}. - * + * * @param pattern the pattern * @param replacement the replacement */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index 563b8b532..b4aa590dd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -28,10 +28,10 @@ * OperationPreprocessors} that can be applied to an {@link Operation Operation's} * {@link OperationRequest request} or {@link OperationResponse response} before it is * documented. - * + * * @author Andy Wilkinson */ -public class Preprocessors { +public final class Preprocessors { private Preprocessors() { @@ -40,7 +40,7 @@ private Preprocessors() { /** * Returns an {@link OperationRequestPreprocessor} that will preprocess the request by * applying the given {@code preprocessors} to it. - * + * * @param preprocessors the preprocessors * @return the request preprocessor */ @@ -52,7 +52,7 @@ public static OperationRequestPreprocessor preprocessRequest( /** * Returns an {@link OperationResponsePreprocessor} that will preprocess the response * by applying the given {@code preprocessors} to it. - * + * * @param preprocessors the preprocessors * @return the response preprocessor */ @@ -64,7 +64,7 @@ public static OperationResponsePreprocessor preprocessResponse( /** * Returns an {@code OperationPreprocessor} that will pretty print the content of the * request or response. - * + * * @return the preprocessor */ public static OperationPreprocessor prettyPrint() { @@ -75,7 +75,7 @@ public static OperationPreprocessor prettyPrint() { /** * Returns an {@code OperationPreprocessor} that will remove headers from the request * or response. - * + * * @param headersToRemove the names of the headers to remove * @return the preprocessor */ @@ -86,7 +86,7 @@ public static OperationPreprocessor removeHeaders(String... headersToRemove) { /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. - * + * * @return the preprocessor */ public static OperationPreprocessor maskLinks() { @@ -96,7 +96,7 @@ public static OperationPreprocessor maskLinks() { /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. - * + * * @param mask the link mask * @return the preprocessor */ @@ -108,8 +108,8 @@ public static OperationPreprocessor maskLinks(String mask) { /** * Returns an {@code OperationPreprocessor} that will modify the content of the * request or response by replacing occurences of the given {@code pattern} with the - * given {@code replacement} - * + * given {@code replacement}. + * * @param pattern the pattern * @param replacement the replacement * @return the preprocessor diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 9e193b3e5..6902e1211 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -34,7 +34,7 @@ /** * A {@link ContentModifier} that modifies the content by pretty printing it. - * + * * @author Andy Wilkinson */ public class PrettyPrintingContentModifier implements ContentModifier { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java index 2a50da0c0..55913531d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java @@ -17,4 +17,5 @@ /** * Support for preprocessing an operation prior to it being documented. */ -package org.springframework.restdocs.operation.preprocess; \ No newline at end of file +package org.springframework.restdocs.operation.preprocess; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java index c237f39a3..2d3767b2d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java @@ -15,6 +15,7 @@ */ /** - * Core Spring REST Docs classes. + * Core Spring REST Docs classes. */ -package org.springframework.restdocs; \ No newline at end of file +package org.springframework.restdocs; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 1aea09df4..c96b58f49 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -46,7 +46,7 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { * {@code -fields}. The fields will be documented using the given * {@code descriptors} and the given {@code attributes} will be included in the model * during template rendering. - * + * * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes @@ -130,7 +130,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { /** * Returns the content type of the request or response extracted from the given * {@code operation}. - * + * * @param operation The operation * @return The content type */ @@ -139,7 +139,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { /** * Returns the content of the request or response extracted form the given * {@code operation}. - * + * * @param operation The operation * @return The content * @throws IOException if the content cannot be extracted @@ -149,7 +149,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { /** * Returns the list of {@link FieldDescriptor FieldDescriptors} that will be used to * generate the documentation. - * + * * @return the field descriptors */ protected final List getFieldDescriptors() { @@ -157,8 +157,8 @@ protected final List getFieldDescriptors() { } /** - * Returns a model for the given {@code descriptor} - * + * Returns a model for the given {@code descriptor}. + * * @param descriptor the descriptor * @return the model */ @@ -172,4 +172,4 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } -} \ No newline at end of file +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java index 0104f143a..ce99ff00f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -19,8 +19,8 @@ import java.util.List; /** - * A handler for the content of a request or response - * + * A handler for the content of a request or response. + * * @author Andy Wilkinson */ interface ContentHandler { @@ -29,7 +29,7 @@ interface ContentHandler { * Finds the fields that are missing from the handler's payload. A field is missing if * it is described by one of the {@code fieldDescriptors} but is not present in the * payload. - * + * * @param fieldDescriptors the descriptors * @return descriptors for the fields that are missing from the payload * @throws PayloadHandlingException if a failure occurs @@ -41,7 +41,7 @@ interface ContentHandler { * are undocumented. A field is undocumented if it is present in the handler's content * but is not described by the given {@code fieldDescriptors}. If the content is * completely documented, {@code null} is returned - * + * * @param fieldDescriptors the descriptors * @return the undocumented content, or {@code null} if all of the content is * documented @@ -52,10 +52,10 @@ interface ContentHandler { /** * Returns the type of the field with the given {@code path} based on the content of * the payload. - * + * * @param path the field path * @return the type of the field */ Object determineFieldType(String path); -} \ No newline at end of file +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 1edd352ab..7df08f1fd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -19,12 +19,11 @@ import org.springframework.restdocs.snippet.AbstractDescriptor; /** - * A description of a field found in a request or response payload - * - * @see PayloadDocumentation#fieldWithPath(String) - * + * A description of a field found in a request or response payload. + * * @author Andreas Evers * @author Andy Wilkinson + * @see PayloadDocumentation#fieldWithPath(String) */ public class FieldDescriptor extends AbstractDescriptor { @@ -46,7 +45,7 @@ protected FieldDescriptor(String path) { /** * Specifies the type of the field. When documenting a JSON payload, the * {@link JsonFieldType} enumeration will typically be used. - * + * * @param type The type of the field * @return {@code this} * @see JsonFieldType @@ -57,8 +56,8 @@ public final FieldDescriptor type(Object type) { } /** - * Marks the field as optional - * + * Marks the field as optional. + * * @return {@code this} */ public final FieldDescriptor optional() { @@ -67,8 +66,8 @@ public final FieldDescriptor optional() { } /** - * Returns the path of the field described by this descriptor - * + * Returns the path of the field described by this descriptor. + * * @return the path */ public final String getPath() { @@ -76,8 +75,8 @@ public final String getPath() { } /** - * Returns the type of the field described by this descriptor - * + * Returns the type of the field described by this descriptor. + * * @return the type */ public final Object getType() { @@ -85,8 +84,8 @@ public final Object getType() { } /** - * Returns {@code true} if the described field is optional, otherwise {@code false} - * + * Returns {@code true} if the described field is optional, otherwise {@code false}. + * * @return {@code true} if the described field is optional, otherwise {@code false} */ public final boolean isOptional() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java index e2cf49064..37d794932 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java @@ -19,7 +19,7 @@ /** * A {@code FieldDoesNotExistException} is thrown when a requested field does not exist in * a payload. - * + * * @author Andy Wilkinson */ @SuppressWarnings("serial") @@ -28,7 +28,7 @@ public class FieldDoesNotExistException extends RuntimeException { /** * Creates a new {@code FieldDoesNotExistException} that indicates that the field with * the given {@code fieldPath} does not exist. - * + * * @param fieldPath the path of the field that does not exist */ public FieldDoesNotExistException(JsonFieldPath fieldPath) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java index c9fa41215..6538491c2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java @@ -19,7 +19,7 @@ /** * A {@code FieldTypeRequiredException} is thrown when a field's type cannot be determined * automatically and, therefore, must be explicitly provided. - * + * * @author Andy Wilkinson */ @SuppressWarnings("serial") @@ -28,7 +28,7 @@ public class FieldTypeRequiredException extends RuntimeException { /** * Creates a new {@code FieldTypeRequiredException} indicating that a type is required * for the reason described in the given {@code message}. - * + * * @param message the message */ public FieldTypeRequiredException(String message) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index e20b4f8d3..fab1d1a5c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -26,7 +26,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; /** - * A {@link ContentHandler} for JSON content + * A {@link ContentHandler} for JSON content. * * @author Andy Wilkinson */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index aca86bcb5..3eefe20c1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -22,8 +22,8 @@ import java.util.regex.Pattern; /** - * A path that identifies a field in a JSON payload - * + * A path that identifies a field in a JSON payload. + * * @author Andy Wilkinson * @author Jeremy Rickard * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index e1aa72d69..7ef5df395 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -24,8 +24,8 @@ /** * A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be - * extracted and removed - * + * extracted and removed. + * * @author Andy Wilkinson * */ @@ -212,8 +212,8 @@ private ProcessingContext(Object payload, JsonFieldPath path) { this(payload, path, null, null); } - private ProcessingContext(Object payload, JsonFieldPath path, List segments, - Match parent) { + private ProcessingContext(Object payload, JsonFieldPath path, + List segments, Match parent) { this.payload = payload; this.path = path; this.segments = segments == null ? path.getSegments() : segments; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java index 0e3b287ad..fcaffa6da 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java @@ -21,13 +21,46 @@ import org.springframework.util.StringUtils; /** - * An enumeration of the possible types for a field in a JSON request or response payload - * + * An enumeration of the possible types for a field in a JSON request or response payload. + * * @author Andy Wilkinson */ public enum JsonFieldType { - ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES; + /** + * An array. + */ + ARRAY, + + /** + * A boolean value. + */ + BOOLEAN, + + /** + * An object (map). + */ + OBJECT, + + /** + * A number. + */ + NUMBER, + + /** + * {@code null}. + */ + NULL, + + /** + * A string. + */ + STRING, + + /** + * A variety of different types. + */ + VARIES; @Override public String toString() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java index ca55dac54..f5bc6ec3e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java @@ -20,8 +20,8 @@ import java.util.Map; /** - * Resolves the type of a field in a JSON request or response payload - * + * Resolves the type of a field in a JSON request or response payload. + * * @author Andy Wilkinson */ class JsonFieldTypeResolver { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index c32df1ba0..653442ed5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -23,7 +23,7 @@ /** * Static factory methods for documenting a RESTful API's request and response payloads. - * + * * @author Andreas Evers * @author Andy Wilkinson */ @@ -43,27 +43,27 @@ private PayloadDocumentation() { * When documenting a JSON payload, the {@code path} uses '.' to descend into a child * object and ' {@code []}' to descend into an array. For example, with this JSON * payload: - * + * *
     	 * {
    -     *    "a":{
    -     *        "b":[
    -     *            {
    -     *                "c":"one"
    -     *            },
    -     *            {
    -     *                "c":"two"
    -     *            },
    -     *            {
    -     *                "d":"three"
    -     *            }
    -     *        ]
    -     *    }
    -     * }
    +	 *    "a":{
    +	 *        "b":[
    +	 *            {
    +	 *                "c":"one"
    +	 *            },
    +	 *            {
    +	 *                "c":"two"
    +	 *            },
    +	 *            {
    +	 *                "d":"three"
    +	 *            }
    +	 *        ]
    +	 *    }
    +	 * }
     	 * 
    - * + * * The following paths are all present: - * + * * * * @@ -90,7 +90,7 @@ private PayloadDocumentation() { * * *
    PathThe string "three"
    - * + * * @param path The path of the field * @return a {@code FieldDescriptor} ready for further configuration */ @@ -108,7 +108,7 @@ public static FieldDescriptor fieldWithPath(String path) { * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. - * + * * @param descriptors The descriptions of the request's fields * @return the handler * @see #fieldWithPath(String) @@ -128,7 +128,7 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. - * + * * @param attributes Attributes made available during rendering of the snippet * @param descriptors The descriptions of the request's fields * @return the handler @@ -149,7 +149,7 @@ public static Snippet requestFields(Map attributes, * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. - * + * * @param descriptors The descriptions of the response's fields * @return the handler * @see #fieldWithPath(String) @@ -169,7 +169,7 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. - * + * * @param attributes Attributes made available during rendering of the snippet * @param descriptors The descriptions of the response's fields * @return the handler diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index 10362718b..383ba2221 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java @@ -17,7 +17,7 @@ package org.springframework.restdocs.payload; /** - * Thrown to indicate that a failure has occurred during payload handling + * Thrown to indicate that a failure has occurred during payload handling. * * @author Andy Wilkinson * @@ -26,7 +26,7 @@ class PayloadHandlingException extends RuntimeException { /** - * Creates a new {@code PayloadHandlingException} with the given cause + * Creates a new {@code PayloadHandlingException} with the given cause. * @param cause the cause of the failure */ PayloadHandlingException(Throwable cause) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 130357faf..354a0283e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -26,7 +26,7 @@ /** * A {@link Snippet} that documents the fields in a request. - * + * * @author Andy Wilkinson * @see PayloadDocumentation#requestFields(FieldDescriptor...) * @see PayloadDocumentation#requestFields(Map, FieldDescriptor...) @@ -36,7 +36,7 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. - * + * * @param descriptors the descriptors */ protected RequestFieldsSnippet(List descriptors) { @@ -47,7 +47,7 @@ protected RequestFieldsSnippet(List descriptors) { * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. - * + * * @param descriptors the descriptors * @param attributes the additional attributes */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 4e829cad8..82015ef8f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.restdocs.payload; import java.io.IOException; @@ -25,7 +26,7 @@ /** * A {@link Snippet} that documents the fields in a response. - * + * * @author Andy Wilkinson * @see PayloadDocumentation#responseFields(FieldDescriptor...) * @see PayloadDocumentation#responseFields(Map, FieldDescriptor...) @@ -35,7 +36,7 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. - * + * * @param descriptors the descriptors */ protected ResponseFieldsSnippet(List descriptors) { @@ -46,7 +47,7 @@ protected ResponseFieldsSnippet(List descriptors) { * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. - * + * * @param descriptors the descriptors * @param attributes the additional attributes */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 53eb22e60..a971f049c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -40,8 +40,8 @@ import org.xml.sax.InputSource; /** - * A {@link ContentHandler} for XML content - * + * A {@link ContentHandler} for XML content. + * * @author Andy Wilkinson */ class XmlContentHandler implements ContentHandler { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java index 1e164515d..f306a0191 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java @@ -17,4 +17,5 @@ /** * Documenting the payload of a RESTful API's requests and responses. */ -package org.springframework.restdocs.payload; \ No newline at end of file +package org.springframework.restdocs.payload; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index f0597fef8..144c8d672 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -45,7 +45,7 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { * the given {@code snippetName} that will document parameters using the given * {@code descriptors}. The given {@code attributes} will be included in the model * during template rendering. - * + * * @param snippetName The snippet name * @param descriptors The descriptors * @param attributes The additional attributes @@ -73,7 +73,7 @@ protected Map createModel(Operation operation) { return model; } - protected void verifyParameterDescriptors(Operation operation) { + private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); Set expectedParameters = this.descriptorsByName.keySet(); Set undocumentedParameters = new HashSet(actualParameters); @@ -84,13 +84,25 @@ protected void verifyParameterDescriptors(Operation operation) { if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { verificationFailed(undocumentedParameters, missingParameters); } - else { - Assert.isTrue(actualParameters.equals(expectedParameters)); - } } + /** + * Extracts the names of the parameters that were present in the given + * {@code operation}. + * + * @param operation the operation + * @return the parameters + */ protected abstract Set extractActualParameters(Operation operation); + /** + * Called when the documented parameters do not match the actual parameters. + * + * @param undocumentedParameters the parameters that were found in the operation but + * were not documented + * @param missingParameters the parameters that were documented but were not found in + * the operation + */ protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); @@ -98,7 +110,7 @@ protected abstract void verificationFailed(Set undocumentedParameters, * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will * be used to generate the documentation key by their * {@link ParameterDescriptor#getName()}. - * + * * @return the map of path descriptors */ protected final Map getFieldDescriptors() { @@ -107,7 +119,7 @@ protected final Map getFieldDescriptors() { /** * Returns a model for the given {@code descriptor}. - * + * * @param descriptor the descriptor * @return the model */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index a1a2840fc..ff9d561dd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -19,8 +19,8 @@ import org.springframework.restdocs.snippet.AbstractDescriptor; /** - * A descriptor of a request or path parameter - * + * A descriptor of a request or path parameter. + * * @author Andy Wilkinson * @see RequestDocumentation#parameterWithName * @@ -32,7 +32,7 @@ public class ParameterDescriptor extends AbstractDescriptor /** * Creates a new {@code ParameterDescriptor} describing the parameter with the given * {@code name}. - * + * * @param name the name of the parameter */ protected ParameterDescriptor(String name) { @@ -40,8 +40,8 @@ protected ParameterDescriptor(String name) { } /** - * Returns the name of the parameter being described by this descriptor - * + * Returns the name of the parameter being described by this descriptor. + * * @return the name of the parameter */ public final String getName() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index b67a9985a..5df99f6f6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -42,7 +42,7 @@ public class PathParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code PathParametersSnippet} that will document the request's path * parameters using the given {@code descriptors}. - * + * * @param descriptors the parameter descriptors */ protected PathParametersSnippet(List descriptors) { @@ -53,7 +53,7 @@ protected PathParametersSnippet(List descriptors) { * Creates a new {@code PathParametersSnippet} that will document the request's path * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. - * + * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 1a0510771..345ec58cc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -27,7 +27,7 @@ /** * Static factory methods for documenting aspects of a request sent to a RESTful API. - * + * * @author Andy Wilkinson */ public abstract class RequestDocumentation { @@ -39,7 +39,7 @@ private RequestDocumentation() { /** * Creates a {@link ParameterDescriptor} that describes a request or path parameter * with the given {@code name}. - * + * * @param name The name of the parameter * @return a {@link ParameterDescriptor} ready for further configuration */ @@ -50,7 +50,7 @@ public static ParameterDescriptor parameterWithName(String name) { /** * Returns a snippet that will document the path parameters from the API call's * request. - * + * * @param descriptors The descriptions of the parameters in the request's path * @return the snippet * @see PathVariable @@ -62,7 +62,7 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { /** * Returns a snippet that will document the path parameters from the API call's * request. The given {@code attributes} will be available during snippet rendering. - * + * * @param attributes Attributes made available during rendering of the path parameters * snippet * @param descriptors The descriptions of the parameters in the request's path @@ -77,7 +77,7 @@ public static Snippet pathParameters(Map attributes, /** * Returns a snippet that will document the request parameters from the API call's * request. - * + * * @param descriptors The descriptions of the request's parameters * @return the snippet * @see RequestParam @@ -90,7 +90,7 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { /** * Returns a snippet that will document the request parameters from the API call's * request. The given {@code attributes} will be available during snippet rendering. - * + * * @param attributes Attributes made available during rendering of the request * parameters snippet * @param descriptors The descriptions of the request's parameters diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index dbe4e372c..5b1f9ba81 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -41,7 +41,7 @@ public class RequestParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code RequestParametersSnippet} that will document the request's * parameters using the given {@code descriptors}. - * + * * @param descriptors the parameter descriptors */ protected RequestParametersSnippet(List descriptors) { @@ -52,7 +52,7 @@ protected RequestParametersSnippet(List descriptors) { * Creates a new {@code RequestParametersSnippet} that will document the request's * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. - * + * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java index 9e78ff299..744aa7f0e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java @@ -17,4 +17,5 @@ /** * Documenting query and path parameters of requests sent to a RESTful API. */ -package org.springframework.restdocs.request; \ No newline at end of file +package org.springframework.restdocs.request; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index 34efc7280..a0b67a3a8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -25,9 +25,8 @@ * Base class for descriptors. Provides the ability to associate arbitrary attributes with * a descriptor. * - * @author Andy Wilkinson - * * @param the type of the descriptor + * @author Andy Wilkinson */ public abstract class AbstractDescriptor> { @@ -36,7 +35,7 @@ public abstract class AbstractDescriptor> { private Object description; /** - * Adds the given {@code attributes} to the descriptor + * Adds the given {@code attributes} to the descriptor. * * @param attributes the attributes * @return the descriptor @@ -50,8 +49,8 @@ public final T attributes(Attribute... attributes) { } /** - * Specifies the description - * + * Specifies the description. + * * @param description the description * @return the descriptor */ @@ -62,8 +61,8 @@ public final T description(Object description) { } /** - * Returns the description - * + * Returns the description. + * * @return the description */ public final Object getDescription() { @@ -71,7 +70,7 @@ public final Object getDescription() { } /** - * Returns the descriptor's attributes + * Returns the descriptor's attributes. * * @return the attributes */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java index db8462f39..b5f1a9c3c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java @@ -13,14 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.restdocs.snippet; import java.util.HashMap; import java.util.Map; /** - * A fluent API for building a map of attributes - * + * A fluent API for building a map of attributes. + * * @author Andy Wilkinson */ public abstract class Attributes { @@ -32,7 +33,7 @@ private Attributes() { /** * Creates an attribute with the given {@code key}. A value for the attribute must * still be specified. - * + * * @param key The key of the attribute * @return An {@code AttributeBuilder} to use to specify the value of the attribute * @see AttributeBuilder#value(Object) @@ -43,7 +44,7 @@ public static AttributeBuilder key(String key) { /** * Creates a {@code Map} of the given {@code attributes}. - * + * * @param attributes The attributes * @return A Map of the attributes */ @@ -56,9 +57,9 @@ public static Map attributes(Attribute... attributes) { } /** - * A simple builder for an attribute (key-value pair) + * A simple builder for an attribute (key-value pair). */ - public static class AttributeBuilder { + public static final class AttributeBuilder { private final String key; @@ -67,8 +68,8 @@ private AttributeBuilder(String key) { } /** - * Configures the value of the attribute - * + * Configures the value of the attribute. + * * @param value The attribute's value * @return A newly created {@code Attribute} */ @@ -81,7 +82,7 @@ public Attribute value(Object value) { /** * An attribute (key-value pair). */ - public static class Attribute { + public static final class Attribute { private final String key; @@ -89,6 +90,7 @@ public static class Attribute { /** * Creates a new attribute with the given {@code key} and {@code value}. + * * @param key the key * @param value the value */ @@ -98,7 +100,8 @@ public Attribute(String key, Object value) { } /** - * Returns the attribute's key + * Returns the attribute's key. + * * @return the key */ public String getKey() { @@ -106,7 +109,8 @@ public String getKey() { } /** - * Returns the attribute's value + * Returns the attribute's value. + * * @return the value */ public Object getValue() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java index d36555273..93c60192f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java @@ -13,23 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.restdocs.snippet; -import org.springframework.restdocs.operation.Operation; +package org.springframework.restdocs.snippet; /** * An exception that can be thrown by a {@link TemplatedSnippet} to indicate that a * failure has occurred during model creation. - * + * * @author Andy Wilkinson - * @see TemplatedSnippet#createModel(Operation) + * @see TemplatedSnippet#createModel(org.springframework.restdocs.operation.Operation) */ @SuppressWarnings("serial") public class ModelCreationException extends RuntimeException { /** * Creates a new {@code ModelCreationException} with the given {@code cause}. - * + * * @param cause the cause */ public ModelCreationException(Throwable cause) { @@ -39,7 +38,7 @@ public ModelCreationException(Throwable cause) { /** * Creates a new {@code ModelCreationException} with the given {@code message} and * {@code cause}. - * + * * @param message the message * @param cause the cause */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 51f63c7b1..908f3d5eb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -46,7 +46,7 @@ * {@link RestDocumentationContext#getTestClass() current test class} formatted using * snake case * - * + * * @author Andy Wilkinson */ public class RestDocumentationContextPlaceholderResolver implements PlaceholderResolver { @@ -58,7 +58,7 @@ public class RestDocumentationContextPlaceholderResolver implements PlaceholderR /** * Creates a new placeholder resolver that will resolve placeholders using the given * {@code context}. - * + * * @param context the context to use */ public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) { @@ -93,7 +93,7 @@ public String resolvePlaceholder(String placeholderName) { /** * Converts the given {@code string} from camelCase to kebab-case. - * + * * @param string the string * @return the converted string */ @@ -103,7 +103,7 @@ protected final String camelCaseToKebabCase(String string) { /** * Converts the given {@code string} from camelCase to snake_case. - * + * * @param string the string * @return the converted string */ @@ -114,7 +114,7 @@ protected final String camelCaseToSnakeCase(String string) { /** * Returns the {@link RestDocumentationContext} that should be used during placeholder * resolution. - * + * * @return the context */ protected final RestDocumentationContext getContext() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java index 63438924d..a43091562 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java @@ -22,14 +22,14 @@ /** * A {@link Snippet} is used to document aspects of a call to a RESTful API. - * + * * @author Andy Wilkinson */ public interface Snippet { /** * Documents the call to the RESTful API described by the given {@code operation}. - * + * * @param operation the API operation * @throws IOException if a failure occurs will documenting the operation */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java index f846b8f32..9928c3522 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java @@ -19,14 +19,15 @@ /** * A {@link RuntimeException} thrown to indicate a problem with the generation of a * documentation snippet. - * + * * @author Andy Wilkinson */ @SuppressWarnings("serial") public class SnippetException extends RuntimeException { /** - * Creates a new {@code SnippetException} described by the given {@code message} + * Creates a new {@code SnippetException} described by the given {@code message}. + * * @param message the message that describes the problem */ public SnippetException(String message) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 2ba5d366d..1ef51ea4e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -28,10 +28,10 @@ /** * Standard implementation of {@link WriterResolver}. - * + * * @author Andy Wilkinson */ -public class StandardWriterResolver implements WriterResolver { +public final class StandardWriterResolver implements WriterResolver { private String encoding = "UTF-8"; @@ -44,7 +44,7 @@ public class StandardWriterResolver implements WriterResolver { * Creates a new {@code StandardWriterResolver} that will use the given * {@code placeholderResolver} to resolve any placeholders in the * {@code operationName}. - * + * * @param placeholderResolver the placeholder resolver */ public StandardWriterResolver(PlaceholderResolver placeholderResolver) { @@ -71,7 +71,7 @@ public void setEncoding(String encoding) { this.encoding = encoding; } - protected File resolveFile(String outputDirectory, String fileName, + File resolveFile(String outputDirectory, String fileName, RestDocumentationContext context) { File outputFile = new File(outputDirectory, fileName); if (!outputFile.isAbsolute()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 3ec4afbb4..015d1fe8e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -42,7 +42,7 @@ public abstract class TemplatedSnippet implements Snippet { * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given * {@code snippetName}. The given {@code attributes} will be included in the model * during rendering of the template. - * + * * @param snippetName The name of the snippet * @param attributes The additional attributes */ @@ -74,7 +74,7 @@ public void document(Operation operation) throws IOException { * given {@code operation}. Any additional attributes that were supplied when this * {@code TemplatedSnippet} were created will be automatically added to the model * prior to rendering. - * + * * @param operation The operation * @return the model * @throws ModelCreationException if model creation fails @@ -84,7 +84,7 @@ public void document(Operation operation) throws IOException { /** * Returns the additional attributes that will be included in the model during * template rendering. - * + * * @return the additional attributes */ protected final Map getAttributes() { @@ -93,11 +93,11 @@ protected final Map getAttributes() { /** * Returns the name of the snippet that will be created. - * + * * @return the snippet name */ protected final String getSnippetName() { return this.snippetName; } -} \ No newline at end of file +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java index 9adca5ca7..29c2393e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -24,7 +24,7 @@ /** * A {@code WriterResolver} is used to access the {@link Writer} that should be used to * write a snippet for an operation that is being documented. - * + * * @author Andy Wilkinson */ public interface WriterResolver { @@ -32,6 +32,7 @@ public interface WriterResolver { /** * Returns a writer that can be used to write the snippet with the given name for the * operation with the given name. + * * @param operationName the name of the operation that is being documented * @param snippetName the name of the snippet * @param restDocumentationContext the current documentation context @@ -43,7 +44,8 @@ Writer resolve(String operationName, String snippetName, /** * Configures the encoding that should be used by any writers produced by this - * resolver + * resolver. + * * @param encoding the encoding */ void setEncoding(String encoding); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java index 1e8540b92..9c16a3e25 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java @@ -17,4 +17,5 @@ /** * Snippet generation. */ -package org.springframework.restdocs.snippet; \ No newline at end of file +package org.springframework.restdocs.snippet; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index eb8fb455e..4e3d38e30 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.restdocs.templates; import org.springframework.core.io.ClassPathResource; @@ -26,7 +27,7 @@ * resource exists {@code default-} is prepended to the name and the classpath is checked * again. The built-in snippet templates are all named {@code default- name}, thereby * allowing them to be overridden. - * + * * @author Andy Wilkinson */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java index 2ab905712..17c2dfc7e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java @@ -22,7 +22,7 @@ /** * A {@code TemplateEngine} is used to render documentation snippets. - * + * * @author Andy Wilkinson * @see MustacheTemplateEngine */ @@ -32,7 +32,7 @@ public interface TemplateEngine { * Compiles the template at the given {@code path}. Typically, a * {@link TemplateResourceResolver} will be used to resolve the path into a resource * that can be read and compiled. - * + * * @param path the path of the template * @return the compiled {@code Template} * @throws IOException if compilation fails diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java index d4259af1c..7def94628 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java @@ -21,17 +21,17 @@ /** * A {@code TemplateResourceResolver} is responsible for resolving a name for a template * into a {@link Resource} from which the template can be read. - * + * * @author Andy Wilkinson */ public interface TemplateResourceResolver { /** * Resolves a {@link Resource} for the template with the given {@code name}. - * + * * @param name the name of the template * @return the {@code Resource} from which the template can be read */ - public Resource resolveTemplateResource(String name); + Resource resolveTemplateResource(String name); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index 524217326..9b359edbd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.restdocs.templates.mustache; import java.util.Map; @@ -22,7 +23,7 @@ /** * An adapter that exposes a compiled Mustache * template as a {@link Template}. - * + * * @author Andy Wilkinson */ public class MustacheTemplate implements Template { @@ -33,6 +34,7 @@ public class MustacheTemplate implements Template { * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. When * rendered, the given {@code defaultContext} will be combined with the render context * prior to executing the delegate. + * * @param delegate The delegate to adapt */ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index 0b16ae81a..df142045a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.restdocs.templates.mustache; import java.io.IOException; @@ -30,7 +31,7 @@ * implemented using JMustache. *

    * Note that JMustache has been repackaged and embedded to prevent classpath conflicts. - * + * * @author Andy Wilkinson */ public class MustacheTemplateEngine implements TemplateEngine { @@ -42,7 +43,7 @@ public class MustacheTemplateEngine implements TemplateEngine { /** * Creates a new {@link MustacheTemplateEngine} that will use the given * {@code templateResourceResolver} to resolve template paths. - * + * * @param templateResourceResolver The resolve to use */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java index 0b4246df3..244b8d9e5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java @@ -17,4 +17,5 @@ /** * JMustache-based implementation of the template API. */ -package org.springframework.restdocs.templates.mustache; \ No newline at end of file +package org.springframework.restdocs.templates.mustache; + diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java index 7f9b1d103..cac448dd7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java @@ -17,4 +17,5 @@ /** * Template API used to render documentation snippets. */ -package org.springframework.restdocs.templates; \ No newline at end of file +package org.springframework.restdocs.templates; + diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index e69192e2a..6968fccff 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -16,10 +16,6 @@ package org.springframework.restdocs.constraints; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.assertThat; - import java.math.BigDecimal; import java.util.Date; @@ -39,8 +35,12 @@ import org.junit.Test; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link ConstraintDescriptions} + * Tests for {@link ConstraintDescriptions}. * * @author Andy Wilkinson */ diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index efb0f2352..8f2fa07d3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -16,10 +16,6 @@ package org.springframework.restdocs.constraints; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.net.URL; import java.util.Collections; import java.util.HashMap; @@ -43,8 +39,12 @@ import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link ResourceBundleConstraintDescriptionResolver} + * Tests for {@link ResourceBundleConstraintDescriptionResolver}. * * @author Andy Wilkinson */ @@ -55,14 +55,14 @@ public class ResourceBundleConstraintDescriptionResolverTests { @Test public void defaultMessageAssertFalse() { String description = this.resolver.resolveDescription(new Constraint( - AssertFalse.class.getName(), Collections. emptyMap())); + AssertFalse.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be false"))); } @Test public void defaultMessageAssertTrue() { String description = this.resolver.resolveDescription(new Constraint( - AssertTrue.class.getName(), Collections. emptyMap())); + AssertTrue.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be true"))); } @@ -98,7 +98,7 @@ public void defaultMessageDigits() { @Test public void defaultMessageFuture() { String description = this.resolver.resolveDescription(new Constraint(Future.class - .getName(), Collections. emptyMap())); + .getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the future"))); } @@ -123,21 +123,21 @@ public void defaultMessageMin() { @Test public void defaultMessageNotNull() { String description = this.resolver.resolveDescription(new Constraint( - NotNull.class.getName(), Collections. emptyMap())); + NotNull.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must not be null"))); } @Test public void defaultMessageNull() { String description = this.resolver.resolveDescription(new Constraint(Null.class - .getName(), Collections. emptyMap())); + .getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be null"))); } @Test public void defaultMessagePast() { String description = this.resolver.resolveDescription(new Constraint(Past.class - .getName(), Collections. emptyMap())); + .getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the past"))); } @@ -179,7 +179,7 @@ public URL getResource(String name) { try { String description = new ResourceBundleConstraintDescriptionResolver() .resolveDescription(new Constraint(NotNull.class.getName(), - Collections. emptyMap())); + Collections.emptyMap())); assertThat(description, is(equalTo("Should not be null"))); } @@ -201,7 +201,7 @@ protected Object[][] getContents() { }; String description = new ResourceBundleConstraintDescriptionResolver(bundle) .resolveDescription(new Constraint(NotNull.class.getName(), Collections - . emptyMap())); + .emptyMap())); assertThat(description, is(equalTo("Not null"))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index e07abd7fe..89bce77b8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -16,11 +16,6 @@ package org.springframework.restdocs.constraints; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.assertThat; - import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -43,8 +38,13 @@ import org.hibernate.validator.constraints.NotBlank; import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link ValidatorConstraintResolver} + * Tests for {@link ValidatorConstraintResolver}. * * @author Andy Wilkinson */ @@ -86,6 +86,10 @@ public void compositeConstraint() { assertThat(constraints, hasSize(1)); } + private ConstraintMatcher constraint(final Class annotation) { + return new ConstraintMatcher(annotation); + } + private static class ConstrainedFields { @NotNull @@ -108,7 +112,7 @@ private static class ConstrainedFields { @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @javax.validation.Constraint(validatedBy = {}) - public @interface CompositeConstraint { + private @interface CompositeConstraint { String message() default "Must be null or not blank"; @@ -118,11 +122,7 @@ private static class ConstrainedFields { } - private ConstraintMatcher constraint(final Class annotation) { - return new ConstraintMatcher(annotation); - } - - private static class ConstraintMatcher extends BaseMatcher { + private static final class ConstraintMatcher extends BaseMatcher { private final Class annotation; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 2e15a1161..f84cf0e18 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -16,13 +16,6 @@ package org.springframework.restdocs.curl; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; - import java.io.IOException; import org.junit.Rule; @@ -38,8 +31,15 @@ import org.springframework.restdocs.test.OperationBuilder; import org.springframework.util.Base64Utils; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; + /** - * Tests for {@link CurlRequestSnippet} + * Tests for {@link CurlRequestSnippet}. * * @author Andy Wilkinson * @author Yann Le Guern @@ -247,8 +247,8 @@ public void customAttributes() throws IOException { this.snippet.expectCurlRequest("custom-attributes").withContents( containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("curl-request")) - .thenReturn( + given(resolver.resolveTemplateResource("curl-request")) + .willReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); new CurlRequestSnippet(attributes(key("title").value("curl request title"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 8da93faab..6dcdcbbe2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -16,16 +16,6 @@ package org.springframework.restdocs.http; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; -import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; -import static org.springframework.web.bind.annotation.RequestMethod.PUT; - import java.io.IOException; import org.junit.Rule; @@ -38,10 +28,18 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import org.springframework.web.bind.annotation.RequestMethod; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; /** - * Tests for {@link HttpRequestSnippet} - * + * Tests for {@link HttpRequestSnippet}. + * * @author Andy Wilkinson * @author Jonathan Pearlin * @@ -56,8 +54,8 @@ public class HttpRequestSnippetTests { @Test public void getRequest() throws IOException { this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( - "Alpha", "a")); + httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, + "localhost").header("Alpha", "a")); new HttpRequestSnippet().document(new OperationBuilder("get-request", this.snippet.getOutputDirectory()).request("http://localhost/foo") @@ -67,7 +65,8 @@ public void getRequest() throws IOException { @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string").withContents( - httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); + httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, + "localhost")); new HttpRequestSnippet().document(new OperationBuilder( "get-request-with-query-string", this.snippet.getOutputDirectory()) @@ -77,8 +76,8 @@ public void getRequestWithQueryString() throws IOException { @Test public void postRequestWithContent() throws IOException { this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( - "Hello, world")); + httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, + "localhost").content("Hello, world")); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-content", this.snippet.getOutputDirectory()) @@ -89,7 +88,8 @@ public void postRequestWithContent() throws IOException { @Test public void postRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("post-request-with-parameter").withContents( - httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost") + httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); @@ -102,8 +102,8 @@ public void postRequestWithParameter() throws IOException { @Test public void putRequestWithContent() throws IOException { this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( - "Hello, world")); + httpRequest(RequestMethod.PUT, "/foo").header(HttpHeaders.HOST, + "localhost").content("Hello, world")); new HttpRequestSnippet().document(new OperationBuilder( "put-request-with-content", this.snippet.getOutputDirectory()) @@ -114,7 +114,8 @@ public void putRequestWithContent() throws IOException { @Test public void putRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("put-request-with-parameter").withContents( - httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost") + httpRequest(RequestMethod.PUT, "/foo") + .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); @@ -129,7 +130,7 @@ public void multipartPost() throws IOException { String expectedContent = createPart(String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post").withContents( - httpRequest(POST, "/upload") + httpRequest(RequestMethod.POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -153,7 +154,7 @@ public void multipartPostWithParameters() throws IOException { + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( - httpRequest(POST, "/upload") + httpRequest(RequestMethod.POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -172,7 +173,7 @@ public void multipartPostWithContentType() throws IOException { .format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( - httpRequest(POST, "/upload") + httpRequest(RequestMethod.POST, "/upload") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -188,7 +189,8 @@ public void multipartPostWithContentType() throws IOException { @Test public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( - httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); + httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, + "api.example.com")); new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", this.snippet.getOutputDirectory()).request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com").build()); @@ -199,8 +201,8 @@ public void requestWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("http-request")) - .thenReturn( + given(resolver.resolveTemplateResource("http-request")) + .willReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index cfe68c60f..ab13d9508 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -16,21 +16,13 @@ package org.springframework.restdocs.http; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.OK; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; - import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -38,8 +30,15 @@ import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; + /** - * Tests for {@link HttpResponseSnippet} + * Tests for {@link HttpResponseSnippet}. * * @author Andy Wilkinson * @author Jonathan Pearlin @@ -51,7 +50,8 @@ public class HttpResponseSnippetTests { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK)); + this.snippet.expectHttpResponse("basic-response").withContents( + httpResponse(HttpStatus.OK)); new HttpResponseSnippet().document(new OperationBuilder("basic-response", this.snippet.getOutputDirectory()).build()); } @@ -59,16 +59,16 @@ public void basicResponse() throws IOException { @Test public void nonOkResponse() throws IOException { this.snippet.expectHttpResponse("non-ok-response").withContents( - httpResponse(BAD_REQUEST)); + httpResponse(HttpStatus.BAD_REQUEST)); new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", - this.snippet.getOutputDirectory()).response().status(BAD_REQUEST.value()) - .build()); + this.snippet.getOutputDirectory()).response() + .status(HttpStatus.BAD_REQUEST.value()).build()); } @Test public void responseWithHeaders() throws IOException { this.snippet.expectHttpResponse("response-with-headers").withContents( - httpResponse(OK) // + httpResponse(HttpStatus.OK) // .header("Content-Type", "application/json") // .header("a", "alpha")); new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", @@ -80,7 +80,7 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(OK).content("content")); + httpResponse(HttpStatus.OK).content("content")); new HttpResponseSnippet().document(new OperationBuilder("response-with-content", this.snippet.getOutputDirectory()).response().content("content").build()); } @@ -90,8 +90,8 @@ public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("http-response")) - .thenReturn( + given(resolver.resolveTemplateResource("http-response")) + .willReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 161fefbf1..5157e07bb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.hypermedia; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -31,9 +28,12 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.StandardOperationResponse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * Tests for {@link ContentTypeLinkExtractor}. - * + * * @author Andy Wilkinson */ public class ContentTypeLinkExtractorTests { @@ -73,5 +73,4 @@ public void extractorCalledWithCompatibleContextType() throws IOException { new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 6711ae19b..02adf5cbb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.hypermedia; -import static org.junit.Assert.assertEquals; - import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -37,6 +35,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import static org.junit.Assert.assertEquals; + /** * Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with * various payloads. @@ -88,14 +88,14 @@ public void multipleLinksWithSameRels() throws IOException { public void noLinks() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("no-links")); - assertLinks(Collections. emptyList(), links); + assertLinks(Collections.emptyList(), links); } @Test public void linksInTheWrongFormat() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("wrong-format")); - assertLinks(Collections. emptyList(), links); + assertLinks(Collections.emptyList(), links); } private void assertLinks(List expectedLinks, Map> actualLinks) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 24bd7ec76..57b9f7556 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -16,14 +16,6 @@ package org.springframework.restdocs.hypermedia; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; - import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -42,8 +34,16 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + /** - * Tests for {@link LinksSnippet} + * Tests for {@link LinksSnippet}. * * @author Andy Wilkinson */ @@ -61,7 +61,7 @@ public void undocumentedLink() throws IOException { this.thrown.expectMessage(equalTo("Links with the following relations were not" + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections. emptyList()).document(new OperationBuilder( + Collections.emptyList()).document(new OperationBuilder( "undocumented-link", this.snippet.getOutputDirectory()).build()); } @@ -77,9 +77,8 @@ public void missingLink() throws IOException { @Test public void documentedOptionalLink() throws IOException { - this.snippet.expectLinks("documented-optional-link").withContents( // - tableWithHeader("Relation", "Description") // - .row("foo", "bar")); + this.snippet.expectLinks("documented-optional-link").withContents( + tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) .document(new OperationBuilder("documented-optional-link", this.snippet @@ -88,9 +87,8 @@ public void documentedOptionalLink() throws IOException { @Test public void missingOptionalLink() throws IOException { - this.snippet.expectLinks("missing-optional-link").withContents( // - tableWithHeader("Relation", "Description") // - .row("foo", "bar")); + this.snippet.expectLinks("missing-optional-link").withContents( + tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") .description("bar").optional())).document(new OperationBuilder( "missing-optional-link", this.snippet.getOutputDirectory()).build()); @@ -110,9 +108,8 @@ public void undocumentedLinkAndMissingLink() throws IOException { @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks("documented-links").withContents( // - tableWithHeader("Relation", "Description") // - .row("a", "one") // + this.snippet.expectLinks("documented-links").withContents( + tableWithHeader("Relation", "Description").row("a", "one") .row("b", "two")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( @@ -124,15 +121,15 @@ public void documentedLinks() throws IOException { @Test public void linksWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( // - tableWithHeader("Relation", "Description", "Foo") // - .row("a", "one", "alpha") // - .row("b", "two", "bravo")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("links")) - .thenReturn( + given(resolver.resolveTemplateResource("links")) + .willReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); + this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( + tableWithHeader("Relation", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one").attributes( @@ -146,21 +143,21 @@ public void linksWithCustomDescriptorAttributes() throws IOException { @Test public void linksWithCustomAttributes() throws IOException { - this.snippet.expectLinks("links-with-custom-attributes").withContents( - startsWith(".Title for the links")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("links")) - .thenReturn( + given(resolver.resolveTemplateResource("links")) + .willReturn( new FileSystemResource( "src/test/resources/custom-snippet-templates/links-with-title.snippet")); + this.snippet.expectLinks("links-with-custom-attributes").withContents( + startsWith(".Title for the links")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")), attributes(key("title").value( - "Title for the links"))) - .document(new OperationBuilder("links-with-custom-attributes", - this.snippet.getOutputDirectory()).attribute( - TemplateEngine.class.getName(), + new LinkDescriptor("b").description("two")), attributes(key("title") + .value("Title for the links"))).document(new OperationBuilder( + "links-with-custom-attributes", this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index f13509b5f..6923b7dbf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -16,10 +16,6 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.net.URI; import java.util.Collections; @@ -34,8 +30,12 @@ import org.springframework.restdocs.operation.StandardOperationRequest; import org.springframework.restdocs.operation.StandardOperationResponse; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link ContentModifyingOperationPreprocessor} + * Tests for {@link ContentModifyingOperationPreprocessor}. * * @author Andy Wilkinson * @@ -57,7 +57,7 @@ public void modifyRequestContent() { StandardOperationRequest request = new StandardOperationRequest( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), new HttpHeaders(), new Parameters(), - Collections. emptyList()); + Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); } @@ -75,7 +75,7 @@ public void unknownContentLengthIsUnchanged() { StandardOperationRequest request = new StandardOperationRequest( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), new HttpHeaders(), new Parameters(), - Collections. emptyList()); + Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(-1L))); } @@ -87,7 +87,7 @@ public void contentLengthIsUpdated() { StandardOperationRequest request = new StandardOperationRequest( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), httpHeaders, new Parameters(), - Collections. emptyList()); + Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(8L))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java index fdfe17c73..622dc5a80 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -16,19 +16,19 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.util.Arrays; import org.junit.Test; import org.springframework.restdocs.operation.OperationRequest; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + /** - * Tests for {@link DelegatingOperationRequestPreprocessor} - * + * Tests for {@link DelegatingOperationRequestPreprocessor}. + * * @author Andy Wilkinson */ public class DelegatingOperationRequestPreprocessorTests { @@ -36,19 +36,17 @@ public class DelegatingOperationRequestPreprocessorTests { @Test public void delegationOccurs() { OperationRequest originalRequest = mock(OperationRequest.class); - OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationRequest preprocessedRequest1 = mock(OperationRequest.class); - when(preprocessor1.preprocess(originalRequest)).thenReturn(preprocessedRequest1); - OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class); OperationRequest preprocessedRequest2 = mock(OperationRequest.class); - when(preprocessor2.preprocess(preprocessedRequest1)).thenReturn( - preprocessedRequest2); - OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); OperationRequest preprocessedRequest3 = mock(OperationRequest.class); - when(preprocessor3.preprocess(preprocessedRequest2)).thenReturn( + + given(preprocessor1.preprocess(originalRequest)).willReturn(preprocessedRequest1); + given(preprocessor2.preprocess(preprocessedRequest1)).willReturn( + preprocessedRequest2); + given(preprocessor3.preprocess(preprocessedRequest2)).willReturn( preprocessedRequest3); OperationRequest result = new DelegatingOperationRequestPreprocessor( @@ -57,4 +55,5 @@ public void delegationOccurs() { assertThat(result, is(preprocessedRequest3)); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java index 764eb63d4..0ddb85a31 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -16,19 +16,19 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.util.Arrays; import org.junit.Test; import org.springframework.restdocs.operation.OperationResponse; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + /** - * Tests for {@link DelegatingOperationResponsePreprocessor} - * + * Tests for {@link DelegatingOperationResponsePreprocessor}. + * * @author Andy Wilkinson */ public class DelegatingOperationResponsePreprocessorTests { @@ -36,20 +36,18 @@ public class DelegatingOperationResponsePreprocessorTests { @Test public void delegationOccurs() { OperationResponse originalResponse = mock(OperationResponse.class); - OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse1 = mock(OperationResponse.class); - when(preprocessor1.preprocess(originalResponse)) - .thenReturn(preprocessedResponse1); - OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse2 = mock(OperationResponse.class); - when(preprocessor2.preprocess(preprocessedResponse1)).thenReturn( - preprocessedResponse2); - OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse3 = mock(OperationResponse.class); - when(preprocessor3.preprocess(preprocessedResponse2)).thenReturn( + + given(preprocessor1.preprocess(originalResponse)).willReturn( + preprocessedResponse1); + given(preprocessor2.preprocess(preprocessedResponse1)).willReturn( + preprocessedResponse2); + given(preprocessor3.preprocess(preprocessedResponse2)).willReturn( preprocessedResponse3); OperationResponse result = new DelegatingOperationResponsePreprocessor( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index cf710c818..808b9ab91 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -16,11 +16,6 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; - import java.net.URI; import java.util.Arrays; import java.util.Collections; @@ -36,8 +31,13 @@ import org.springframework.restdocs.operation.StandardOperationRequest; import org.springframework.restdocs.operation.StandardOperationResponse; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link HeaderRemovingOperationPreprocessorTests} + * Tests for {@link HeaderRemovingOperationPreprocessorTests}. * * @author Andy Wilkinson * @@ -52,7 +52,7 @@ public void modifyRequestHeaders() { StandardOperationRequest request = new StandardOperationRequest( URI.create("http://localhost"), HttpMethod.GET, new byte[0], getHttpHeaders(), new Parameters(), - Collections. emptyList()); + Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index aa881ba80..e63b887c3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -16,10 +16,6 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; @@ -34,8 +30,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link LinkMaskingContentModifier} + * Tests for {@link LinkMaskingContentModifier}. * * @author Andy Wilkinson * @@ -125,21 +125,22 @@ private HalPayload createHalPayload(Link... links) { return payload; } - static final class AtomPayload { + private static final class AtomPayload { private List links; - public void setLinks(List links) { - this.links = links; - } - + @SuppressWarnings("unused") public List getLinks() { return this.links; } + public void setLinks(List links) { + this.links = links; + } + } - static final class HalPayload { + private static final class HalPayload { private Map links; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index c7a1e2c93..de9ba056f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -16,16 +16,16 @@ package org.springframework.restdocs.operation.preprocess; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.util.regex.Pattern; import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link PatternReplacingContentModifier} + * Tests for {@link PatternReplacingContentModifier}. * * @author Andy Wilkinson * diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 8e6aa197d..abedadf30 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -16,14 +16,14 @@ package org.springframework.restdocs.operation.preprocess; +import org.junit.Test; + import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; -import org.junit.Test; - /** - * Tests for {@link PrettyPrintingContentModifier} - * + * Tests for {@link PrettyPrintingContentModifier}. + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index f219abcec..043977968 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -16,16 +16,16 @@ package org.springframework.restdocs.payload; +import org.junit.Test; + import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import org.junit.Test; - /** - * Tests for {@link JsonFieldPath} - * + * Tests for {@link JsonFieldPath}. + * * @author Andy Wilkinson * @author Jeremy Rickard */ @@ -113,17 +113,20 @@ public void compilationOfPathStartingWithAnArray() { @Test public void compilationOfMultipleElementPathWithBrackets() { - assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments(), contains("a", "b", "c")); + assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments(), + contains("a", "b", "c")); } @Test public void compilationOfMultipleElementPathWithAndWithoutBrackets() { - assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments(), contains("a", "[]", "b", "c")); + assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments(), + contains("a", "[]", "b", "c")); } @Test public void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { - assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments(), contains("a.key", "[]", "b", "c")); + assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments(), + contains("a.key", "[]", "b", "c")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 35ed2d64e..d48212f00 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.payload; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - import java.io.IOException; import java.util.Arrays; import java.util.HashMap; @@ -29,9 +26,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link JsonFieldProcessor} - * + * Tests for {@link JsonFieldProcessor}. + * * @author Andy Wilkinson */ public class JsonFieldProcessorTests { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java index 468689512..7ef74aa7f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java @@ -16,9 +16,6 @@ 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; @@ -28,9 +25,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link JsonFieldTypeResolver} - * + * Tests for {@link JsonFieldTypeResolver}. + * * @author Andy Wilkinson * */ @@ -74,7 +74,8 @@ public void stringField() throws IOException { @Test public void nestedField() throws IOException { assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c", - createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT)); + createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), + equalTo(JsonFieldType.OBJECT)); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 7e603a5b3..1c1481c80 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -16,16 +16,6 @@ package org.springframework.restdocs.payload; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; - import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -43,9 +33,19 @@ import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + /** - * Tests for {@link RequestFieldsSnippet} - * + * Tests for {@link RequestFieldsSnippet}. + * * @author Andy Wilkinson */ public class RequestFieldsSnippetTests { @@ -58,10 +58,9 @@ public class RequestFieldsSnippetTests { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("a.b", "Number", "one") // - .row("a.c", "String", "two") // + this.snippet.expectRequestFields("map-request-with-fields").withContents( + tableWithHeader("Path", "Type", "Description") + .row("a.b", "Number", "one").row("a.c", "String", "two") .row("a", "Object", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), @@ -74,10 +73,9 @@ public void mapRequestWithFields() throws IOException { @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]a.b", "Number", "one") // - .row("[]a.c", "String", "two") // + this.snippet.expectRequestFields("array-request-with-fields").withContents( + tableWithHeader("Path", "Type", "Description") + .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") .row("[]a", "Object", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), @@ -94,7 +92,7 @@ public void undocumentedRequestField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections. emptyList()) + new RequestFieldsSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-request-field", this.snippet .getOutputDirectory()).request("http://localhost") .content("{\"a\": 5}").build()); @@ -140,15 +138,16 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException @Test public void requestFieldsWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestFields( - "request-fields-with-custom-descriptor-attributes").withContents( // - tableWithHeader("Path", "Type", "Description", "Foo") // - .row("a.b", "Number", "one", "alpha") // - .row("a.c", "String", "two", "bravo") // - .row("a", "Object", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-fields")).thenReturn( + given(resolver.resolveTemplateResource("request-fields")).willReturn( snippetResource("request-fields-with-extra-column")); + this.snippet.expectRequestFields( + "request-fields-with-custom-descriptor-attributes").withContents( + tableWithHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); + new RequestFieldsSnippet(Arrays.asList( fieldWithPath("a.b").description("one").attributes( key("foo").value("alpha")), @@ -165,11 +164,12 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void requestFieldsWithCustomAttributes() throws IOException { - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-fields")).thenReturn( + given(resolver.resolveTemplateResource("request-fields")).willReturn( snippetResource("request-fields-with-title")); + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes(key("title").value("Custom title"))) .document(new OperationBuilder("request-fields-with-custom-attributes", @@ -181,15 +181,16 @@ public void requestFieldsWithCustomAttributes() throws IOException { @Test public void requestFieldsWithListDescription() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-fields")).willReturn( + snippetResource("request-fields-with-list-description")); this.snippet.expectRequestFields("request-fields-with-list-description") .withContents( tableWithHeader("Path", "Type", "Description") // .row("a", "String", String.format(" - one%n - two")) .configuration("[cols=\"1,1,1a\"]")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-fields")).thenReturn( - snippetResource("request-fields-with-list-description")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( Arrays.asList("one", "two")))) .document(new OperationBuilder("request-fields-with-list-description", @@ -201,11 +202,9 @@ public void requestFieldsWithListDescription() throws IOException { @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields("xml-request").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("a/b", "b", "one") // - .row("a/c", "c", "two") // - .row("a", "a", "three")); + this.snippet.expectRequestFields("xml-request").withContents( + tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") + .row("a/c", "c", "two").row("a", "a", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), @@ -224,7 +223,7 @@ public void undocumentedXmlRequestField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections. emptyList()) + new RequestFieldsSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-xml-request-field", this.snippet.getOutputDirectory()) .request("http://localhost") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index e8459d7f8..56bf56886 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -13,17 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.restdocs.payload; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +package org.springframework.restdocs.payload; import java.io.IOException; import java.util.Arrays; @@ -42,8 +33,18 @@ import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + /** - * Tests for {@link ReponseFieldsSnippet} + * Tests for {@link ResponseFieldsSnippet}. * * @author Andy Wilkinson */ @@ -57,13 +58,11 @@ public class ResponseFieldsSnippetTests { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields("map-response-with-fields").withContents(// - tableWithHeader("Path", "Type", "Description") // - .row("id", "Number", "one") // - .row("date", "String", "two") // - .row("assets", "Array", "three") // - .row("assets[]", "Object", "four") // - .row("assets[].id", "Number", "five") // + this.snippet.expectResponseFields("map-response-with-fields").withContents( + tableWithHeader("Path", "Type", "Description").row("id", "Number", "one") + .row("date", "String", "two").row("assets", "Array", "three") + .row("assets[]", "Object", "four") + .row("assets[].id", "Number", "five") .row("assets[].name", "String", "six")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets") @@ -82,10 +81,9 @@ public void mapResponseWithFields() throws IOException { @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]a.b", "Number", "one") // - .row("[]a.c", "String", "two") // + this.snippet.expectResponseFields("array-response-with-fields").withContents( + tableWithHeader("Path", "Type", "Description") + .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") .row("[]a", "Object", "three")); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") @@ -98,9 +96,10 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { - this.snippet.expectResponseFields("array-response").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("[]", "String", "one")); + this.snippet.expectResponseFields("array-response") + .withContents( + tableWithHeader("Path", "Type", "Description").row("[]", + "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) .document(new OperationBuilder("array-response", this.snippet .getOutputDirectory()).response() @@ -109,15 +108,16 @@ public void arrayResponse() throws IOException { @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents( // - tableWithHeader("Path", "Type", "Description", "Foo") // - .row("a.b", "Number", "one", "alpha") // - .row("a.c", "String", "two", "bravo") // - .row("a", "Object", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("response-fields")).thenReturn( + given(resolver.resolveTemplateResource("response-fields")).willReturn( snippetResource("response-fields-with-extra-column")); + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents( + tableWithHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); + new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("a.b").description("one").attributes( key("foo").value("alpha")), @@ -134,11 +134,12 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void responseFieldsWithCustomAttributes() throws IOException { - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("response-fields")).thenReturn( + given(resolver.resolveTemplateResource("response-fields")).willReturn( snippetResource("response-fields-with-title")); + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents(startsWith(".Custom title")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes(key("title").value("Custom title"))) .document(new OperationBuilder("response-fields-with-custom-attributes", @@ -150,11 +151,9 @@ public void responseFieldsWithCustomAttributes() throws IOException { @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields("xml-response").withContents( // - tableWithHeader("Path", "Type", "Description") // - .row("a/b", "b", "one") // - .row("a/c", "c", "two") // - .row("a", "a", "three")); + this.snippet.expectResponseFields("xml-response").withContents( + tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") + .row("a/c", "c", "two").row("a", "a", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) @@ -172,7 +171,7 @@ public void undocumentedXmlResponseField() throws IOException { this.thrown .expectMessage(startsWith("The following parts of the payload were not" + " documented:")); - new ResponseFieldsSnippet(Collections. emptyList()) + new ResponseFieldsSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-xml-response-field", this.snippet.getOutputDirectory()) .response() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 5d858d936..f7f0f462c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -16,16 +16,6 @@ package org.springframework.restdocs.request; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; - import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -41,10 +31,20 @@ import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; + /** - * Tests for {@link PathParametersSnippet} - * - * @author awilkinson + * Tests for {@link PathParametersSnippet}. + * + * @author Andy Wilkinson * */ public class PathParametersSnippetTests { @@ -60,7 +60,7 @@ public void undocumentedPathParameter() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not documented: [a]")); - new PathParametersSnippet(Collections. emptyList()) + new PathParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-path-parameter", this.snippet.getOutputDirectory()).attribute( "org.springframework.restdocs.urlTemplate", "/{a}/").build()); @@ -121,13 +121,14 @@ public void pathParametersWithQueryString() throws IOException { @Test public void pathParametersWithCustomDescriptorAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("path-parameters")).willReturn( + snippetResource("path-parameters-with-extra-column")); this.snippet.expectPathParameters( "path-parameters-with-custom-descriptor-attributes").withContents( tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("path-parameters")).thenReturn( - snippetResource("path-parameters-with-extra-column")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) @@ -141,16 +142,15 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { @Test public void pathParametersWithCustomAttributes() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("path-parameters")).thenReturn( + given(resolver.resolveTemplateResource("path-parameters")).willReturn( snippetResource("path-parameters-with-title")); - new PathParametersSnippet( - Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), + this.snippet.expectPathParameters("path-parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), parameterWithName("b") + .description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) .document(new OperationBuilder("path-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 330d411c5..9ac769721 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -16,15 +16,6 @@ package org.springframework.restdocs.request; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; - import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -40,8 +31,17 @@ import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + /** - * Tests for {@link RequestParametersSnippet} + * Tests for {@link RequestParametersSnippet}. * * @author Andy Wilkinson */ @@ -59,7 +59,7 @@ public void undocumentedParameter() throws IOException { this.thrown .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); - new RequestParametersSnippet(Collections. emptyList()) + new RequestParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-parameter", this.snippet .getOutputDirectory()).request("http://localhost") .param("a", "alpha").build()); @@ -104,13 +104,14 @@ public void requestParameters() throws IOException { @Test public void requestParametersWithCustomDescriptorAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parameters")).willReturn( + snippetResource("request-parameters-with-extra-column")); this.snippet.expectRequestParameters( "request-parameters-with-custom-descriptor-attributes").withContents( tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-parameters")).thenReturn( - snippetResource("request-parameters-with-extra-column")); + new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), @@ -125,24 +126,23 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException @Test public void requestParametersWithCustomAttributes() throws IOException { - this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - when(resolver.resolveTemplateResource("request-parameters")).thenReturn( + given(resolver.resolveTemplateResource("request-parameters")).willReturn( snippetResource("request-parameters-with-title")); - new RequestParametersSnippet( - Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(new OperationBuilder( - "request-parameters-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").param("a", "alpha") - .param("b", "bravo").build()); + this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") + .withContents(startsWith(".The title")); + + new RequestParametersSnippet(Arrays.asList( + parameterWithName("a").description("one").attributes( + key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo"))), attributes(key("title").value( + "The title"))).document(new OperationBuilder( + "request-parameters-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .param("a", "alpha").param("b", "bravo").build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 37c19160a..b70e936d4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -16,15 +16,15 @@ package org.springframework.restdocs.snippet; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + /** - * Tests for {@link RestDocumentationContextPlaceholderResolver} + * Tests for {@link RestDocumentationContextPlaceholderResolver}. * * @author Andy Wilkinson * diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 2f186dd0d..55ae2218e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -16,20 +16,20 @@ package org.springframework.restdocs.snippet; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; - import java.io.File; import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + /** * Tests for {@link StandardWriterResolver}. - * + * * @author Andy Wilkinson */ public class StandardWriterResolverTests { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index bb221c764..ef807ee20 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.restdocs.templates; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +package org.springframework.restdocs.templates; import java.net.URL; import java.util.HashMap; @@ -29,9 +26,13 @@ import org.junit.rules.ExpectedException; import org.springframework.core.io.Resource; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** * Tests for {@link TemplateResourceResolver}. - * + * * @author Andy Wilkinson */ public class StandardTemplateResourceResolverTests { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 9786aa206..c20b0df06 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -16,9 +16,6 @@ package org.springframework.restdocs.test; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - import java.io.File; import java.io.IOException; @@ -29,10 +26,13 @@ import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + /** * The {@code ExpectedSnippet} rule is used to verify that a {@link TemplatedSnippet} has * generated the expected snippet. - * + * * @author Andy Wilkinson */ public class ExpectedSnippet implements TestRule { @@ -52,22 +52,6 @@ public Statement apply(final Statement base, Description description) { return new ExpectedSnippetStatement(base); } - private final class ExpectedSnippetStatement extends Statement { - - private final Statement delegate; - - public ExpectedSnippetStatement(Statement delegate) { - this.delegate = delegate; - } - - @Override - public void evaluate() throws Throwable { - this.delegate.evaluate(); - verifySnippet(); - } - - } - private void verifySnippet() throws IOException { if (this.outputDirectory != null && this.expectedName != null) { File snippetDir = new File(this.outputDirectory, this.expectedName); @@ -130,4 +114,20 @@ public File getOutputDirectory() { return this.outputDirectory; } + private final class ExpectedSnippetStatement extends Statement { + + private final Statement delegate; + + private ExpectedSnippetStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + this.delegate.evaluate(); + verifySnippet(); + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e28d5751d..50d6cd83c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -43,6 +43,11 @@ import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +/** + * Basic builder API for creating an {@link Operation}. + * + * @author Andy Wilkinson + */ public class OperationBuilder { private final Map attributes = new HashMap<>(); @@ -91,7 +96,10 @@ public Operation build() { this.responseBuilder.buildResponse(), this.attributes); } - public class OperationRequestBuilder { + /** + * Basic builder API for creating an {@link OperationRequest}. + */ + public final class OperationRequestBuilder { private URI requestUri = URI.create("http://localhost/"); @@ -105,7 +113,7 @@ public class OperationRequestBuilder { private List partBuilders = new ArrayList<>(); - public OperationRequestBuilder(String uri) { + private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); } @@ -151,7 +159,10 @@ public OperationRequestPartBuilder part(String name, byte[] content) { return partBuilder; } - public class OperationRequestPartBuilder { + /** + * Basic builder API for creating an {@link OperationRequestPart}. + */ + public final class OperationRequestPartBuilder { private final String name; @@ -191,7 +202,10 @@ public OperationRequestPartBuilder header(String name, String value) { } } - public class OperationResponseBuilder { + /** + * Basic builder API for creating an {@link OperationResponse}. + */ + public final class OperationResponseBuilder { private HttpStatus status = HttpStatus.OK; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 444b2b53f..8bbb6de37 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -35,10 +35,14 @@ /** * {@link Matcher Matchers} for verify the contents of generated documentation snippets. - * + * * @author Andy Wilkinson */ -public class SnippetMatchers { +public final class SnippetMatchers { + + private SnippetMatchers() { + + } public static SnippetMatcher snippet() { return new SnippetMatcher(); @@ -117,6 +121,11 @@ private String getLinesAsString() { } } + /** + * A {@link Matcher} for an Asciidoctor code block. + * + * @param The type of the matcher + */ public static class AsciidoctorCodeBlockMatcher> extends AbstractSnippetContentMatcher { @@ -134,6 +143,11 @@ public T content(String content) { } + /** + * A {@link Matcher} for an HTTP request or response. + * + * @param The type of the matcher + */ public static abstract class HttpMatcher> extends AsciidoctorCodeBlockMatcher> { @@ -151,25 +165,36 @@ public T header(String name, String value) { } - public static class HttpResponseMatcher extends HttpMatcher { + /** + * A {@link Matcher} for an HTTP response. + */ + public static final class HttpResponseMatcher extends + HttpMatcher { - public HttpResponseMatcher(HttpStatus status) { + private HttpResponseMatcher(HttpStatus status) { this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); this.content(""); } } - public static class HttpRequestMatcher extends HttpMatcher { + /** + * A {@link Matcher} for an HTTP request. + */ + public static final class HttpRequestMatcher extends HttpMatcher { - public HttpRequestMatcher(RequestMethod requestMethod, String uri) { + private HttpRequestMatcher(RequestMethod requestMethod, String uri) { this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); this.content(""); } } - public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher { + /** + * A {@link Matcher} for an Asciidoctor table. + */ + public static final class AsciidoctorTableMatcher extends + AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String title, String... columns) { if (StringUtils.hasText(title)) { @@ -198,6 +223,9 @@ public AsciidoctorTableMatcher configuration(String configuration) { } } + /** + * A {@link Matcher} for a snippet file. + */ public static class SnippetMatcher extends BaseMatcher { private Matcher expectedContents; diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index b9206d842..000000000 --- a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,305 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.codeComplete.argumentPrefixes= -org.eclipse.jdt.core.codeComplete.argumentSuffixes= -org.eclipse.jdt.core.codeComplete.fieldPrefixes= -org.eclipse.jdt.core.codeComplete.fieldSuffixes= -org.eclipse.jdt.core.codeComplete.localPrefixes= -org.eclipse.jdt.core.codeComplete.localSuffixes= -org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= -org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= -org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= -org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.7 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=1 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=false -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=false -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=90 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=8 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=90 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true \ No newline at end of file diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs index 3f14d6355..c5e23c905 100644 --- a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs +++ b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs @@ -50,21 +50,27 @@ cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access=false cleanup.use_this_for_non_static_field_access_only_if_necessary=false cleanup.use_this_for_non_static_method_access=false cleanup.use_this_for_non_static_method_access_only_if_necessary=true cleanup.use_type_arguments=false -cleanup_profile=_Spring Rest Docs Cleanup Conventions +cleanup_profile=_Spring REST Docs Cleanup Conventions cleanup_settings_version=2 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring Rest Docs Java Conventions +formatter_profile=_Spring REST Docs Java Conventions formatter_settings_version=12 org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=true -org.eclipse.jdt.ui.keywordthis=true +org.eclipse.jdt.ui.gettersetter.use.is=false +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; +org.eclipse.jdt.ui.javadoc=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true +org.eclipse.jdt.ui.staticondemandthreshold=9999 +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true @@ -75,22 +81,20 @@ sp_cleanup.add_missing_override_annotations=true sp_cleanup.add_missing_override_annotations_interface_methods=true sp_cleanup.add_serial_version_id=false sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_parentheses_in_expressions=true sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=true -sp_cleanup.convert_functional_interfaces=false +sp_cleanup.always_use_this_for_non_static_method_access=false sp_cleanup.convert_to_enhanced_for_loop=false sp_cleanup.correct_indentation=false sp_cleanup.format_source_code=true sp_cleanup.format_source_code_changes_only=false -sp_cleanup.insert_inferred_type_arguments=false -sp_cleanup.make_local_variable_final=true +sp_cleanup.make_local_variable_final=false sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=true +sp_cleanup.make_private_fields_final=false sp_cleanup.make_type_abstract_if_missing_method=false sp_cleanup.make_variable_declarations_final=false sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.never_use_parentheses_in_expressions=false sp_cleanup.on_save_use_additional_actions=true sp_cleanup.organize_imports=true sp_cleanup.qualify_static_field_accesses_with_declaring_class=false @@ -99,13 +103,12 @@ sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class= sp_cleanup.qualify_static_member_accesses_with_declaring_class=true sp_cleanup.qualify_static_method_accesses_with_declaring_class=false sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_redundant_type_arguments=true -sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces=true sp_cleanup.remove_trailing_whitespaces_all=true sp_cleanup.remove_trailing_whitespaces_ignore_empty=false sp_cleanup.remove_unnecessary_casts=true sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_imports=true sp_cleanup.remove_unused_local_variables=false sp_cleanup.remove_unused_private_fields=true sp_cleanup.remove_unused_private_members=false @@ -113,13 +116,10 @@ sp_cleanup.remove_unused_private_methods=true sp_cleanup.remove_unused_private_types=true sp_cleanup.sort_members=false sp_cleanup.sort_members_all=false -sp_cleanup.use_anonymous_class_creation=false -sp_cleanup.use_blocks=false +sp_cleanup.use_blocks=true sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_lambda=true sp_cleanup.use_parentheses_in_expressions=false sp_cleanup.use_this_for_non_static_field_access=true sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=false -sp_cleanup.use_type_arguments=false \ No newline at end of file +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java index ebb0a4c48..f8effd0a9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java @@ -21,13 +21,14 @@ /** * Abstract configurer that declares methods that are internal to the documentation * configuration implementation. - * + * * @author Andy Wilkinson */ abstract class AbstractConfigurer { /** - * Applies the configuration, possibly by modifying the given {@code request} + * Applies the configuration, possibly by modifying the given {@code request}. + * * @param request the request that may be modified */ abstract void apply(MockHttpServletRequest request); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java index 57153d70a..a4444e7cb 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java @@ -23,9 +23,9 @@ /** * Base class for {@link NestedConfigurer} implementations. - * - * @author Andy Wilkinson + * * @param The type of the configurer's parent + * @author Andy Wilkinson */ abstract class AbstractNestedConfigurer extends AbstractConfigurer implements NestedConfigurer, MockMvcConfigurer { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java index 5e7a7580e..d80edb13b 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java @@ -21,10 +21,9 @@ /** * An adapter to expose an {@link Enumeration} as an {@link Iterable}. - * - * @author Andy Wilkinson * * @param the type of the Enumeration's contents + * @author Andy Wilkinson */ final class IterableEnumeration implements Iterable { @@ -57,8 +56,8 @@ public void remove() { } /** - * Creates a new {@code Iterable} that will iterate over the given {@code enumeration} - * + * Creates an {@code Iterable} that will iterate over the given {@code enumeration}. + * * @param the type of the enumeration's elements * @param enumeration The enumeration to expose as an {@code Iterable} * @return the iterable diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 4d1c4a9f9..417f29e82 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; - import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; @@ -43,10 +41,12 @@ import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; + /** * A factory for creating an {@link OperationRequest} from a * {@link MockHttpServletRequest}. - * + * * @author Andy Wilkinson * */ @@ -62,7 +62,7 @@ class MockMvcOperationRequestFactory { /** * Creates a new {@code OperationRequest} derived from the given {@code mockRequest}. - * + * * @param mockRequest the request * @return the {@code OperationRequest} * @throws Exception if the request could not be created diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java index 0f6f6b8ab..549156976 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java @@ -25,14 +25,14 @@ /** * A factory for creating an {@link OperationResponse} derived from a * {@link MockHttpServletResponse}. - * + * * @author Andy Wilkinson */ class MockMvcOperationResponseFactory { /** * Create a new {@code OperationResponse} derived from the given {@code mockResponse}. - * + * * @param mockResponse the response * @return the {@code OperationResponse} */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 2a2cc2d6e..a41ed1cf1 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -26,8 +26,8 @@ import org.springframework.test.web.servlet.setup.MockMvcConfigurer; /** - * Static factory methods for documenting RESTful APIs using Spring MVC Test - * + * Static factory methods for documenting RESTful APIs using Spring MVC Test. + * * @author Andy Wilkinson */ public abstract class MockMvcRestDocumentation { @@ -39,7 +39,7 @@ private MockMvcRestDocumentation() { /** * Provides access to a {@link MockMvcConfigurer} that can be used to configure a * {@link MockMvc} instance using the given {@code restDocumentation}. - * + * * @param restDocumentation the REST documentation * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) @@ -52,7 +52,7 @@ public static RestDocumentationMockMvcConfigurer documentationConfiguration( /** * Documents the API call with the given {@code identifier} using the given * {@code snippets}. - * + * * @param identifier an identifier for the API call that is being documented * @param snippets the snippets that will document the API call * @return a Mock MVC {@code ResultHandler} that will produce the documentation @@ -68,7 +68,7 @@ public static RestDocumentationResultHandler document(String identifier, * Documents the API call with the given {@code identifier} using the given * {@code snippets}. The given {@code requestPreprocessor} is applied to the request * before it is documented. - * + * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param snippets the snippets that will document the API call @@ -86,7 +86,7 @@ public static RestDocumentationResultHandler document(String identifier, * Documents the API call with the given {@code identifier} using the given * {@code snippets}. The given {@code responsePreprocessor} is applied to the request * before it is documented. - * + * * @param identifier an identifier for the API call that is being documented * @param responsePreprocessor the response preprocessor * @param snippets the snippets that will document the API call @@ -105,7 +105,7 @@ public static RestDocumentationResultHandler document(String identifier, * {@code snippets}. The given {@code requestPreprocessor} and * {@code responsePreprocessor} are applied to the request and response respectively * before they are documented. - * + * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param responsePreprocessor the response preprocessor diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java index 2fc73bb37..1c6768c8f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java @@ -20,15 +20,15 @@ /** * A configurer that is nested and, therefore, has a parent. - * - * @author Andy Wilkinson + * * @param The parent's type + * @author Andy Wilkinson */ interface NestedConfigurer { /** - * Returns the configurer's parent - * + * Returns the configurer's parent. + * * @return the parent */ PARENT and(); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index e13025eb3..0fd6d7caa 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -33,8 +33,8 @@ import org.springframework.web.context.WebApplicationContext; /** - * A {@link MockMvcConfigurer} that can be used to configure the documentation - * + * A {@link MockMvcConfigurer} that can be used to configure the documentation. + * * @author Andy Wilkinson * @author Dmitriy Mayboroda * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) @@ -55,7 +55,7 @@ public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter /** * Creates a new {code RestDocumentationMockMvcConfigurer} that will use the given * {@code restDocumentation} when configuring MockMvc. - * + * * @param restDocumentation the rest documentation * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) */ @@ -69,7 +69,7 @@ this.snippetConfigurer, new ContentLengthHeaderConfigurer(), /** * Returns a {@link UriConfigurer} that can be used to configure the request URIs that * will be documented. - * + * * @return the URI configurer */ public UriConfigurer uris() { @@ -79,7 +79,7 @@ public UriConfigurer uris() { /** * Returns a {@link SnippetConfigurer} that can be used to configure the snippets that * will be generated. - * + * * @return the snippet configurer */ public SnippetConfigurer snippets() { @@ -88,7 +88,7 @@ public SnippetConfigurer snippets() { /** * Configures the {@link TemplateEngine} that will be used for snippet rendering. - * + * * @param templateEngine the template engine to use * @return {@code this} */ @@ -100,7 +100,7 @@ public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine template /** * Configures the {@link WriterResolver} that will be used to resolve a writer for a * snippet. - * + * * @param writerResolver The writer resolver to use * @return {@code this} */ @@ -115,7 +115,7 @@ public RequestPostProcessor beforeMockMvcCreated( return this.requestPostProcessor; } - private static class ContentLengthHeaderConfigurer extends AbstractConfigurer { + private static final class ContentLengthHeaderConfigurer extends AbstractConfigurer { @Override void apply(MockHttpServletRequest request) { @@ -128,7 +128,7 @@ void apply(MockHttpServletRequest request) { } - private static class TemplateEngineConfigurer extends AbstractConfigurer { + private static final class TemplateEngineConfigurer extends AbstractConfigurer { private TemplateEngine templateEngine = new MustacheTemplateEngine( new StandardTemplateResourceResolver()); @@ -144,7 +144,7 @@ void setTemplateEngine(TemplateEngine templateEngine) { } - private static class WriterResolverConfigurer extends AbstractConfigurer { + private static final class WriterResolverConfigurer extends AbstractConfigurer { private WriterResolver writerResolver; @@ -167,7 +167,7 @@ void setWriterResolver(WriterResolver writerResolver) { } - private static class ConfigurerApplyingRequestPostProcessor implements + private static final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { private final RestDocumentation restDocumentation; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index dc6f7a25c..2c9dc4529 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -29,7 +29,7 @@ * template and makes it available for documentation. Required when * {@link RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...) * ) documenting path parameters} and recommended for general usage. - * + * * @author Andy Wilkinson * @see MockMvcRequestBuilders * @see RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...) @@ -47,7 +47,7 @@ private RestDocumentationRequestBuilders() { /** * Create a {@link MockHttpServletRequestBuilder} for a GET request. The url template * will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the GET request @@ -60,7 +60,7 @@ public static MockHttpServletRequestBuilder get(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a GET request. - * + * * @param uri the URL * @return the builder for the GET request */ @@ -71,7 +71,7 @@ public static MockHttpServletRequestBuilder get(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a POST request. The url template * will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the POST request @@ -84,7 +84,7 @@ public static MockHttpServletRequestBuilder post(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a POST request. - * + * * @param uri the URL * @return the builder for the POST request */ @@ -95,7 +95,7 @@ public static MockHttpServletRequestBuilder post(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a PUT request. The url template * will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the PUT request @@ -108,7 +108,7 @@ public static MockHttpServletRequestBuilder put(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a PUT request. - * + * * @param uri the URL * @return the builder for the PUT request */ @@ -119,7 +119,7 @@ public static MockHttpServletRequestBuilder put(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. The url * template will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the PATCH request @@ -132,7 +132,7 @@ public static MockHttpServletRequestBuilder patch(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. - * + * * @param uri the URL * @return the builder for the PATCH request */ @@ -143,7 +143,7 @@ public static MockHttpServletRequestBuilder patch(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. The url * template will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the DELETE request @@ -156,7 +156,7 @@ public static MockHttpServletRequestBuilder delete(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. - * + * * @param uri the URL * @return the builder for the DELETE request */ @@ -167,7 +167,7 @@ public static MockHttpServletRequestBuilder delete(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. The url * template will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the OPTIONS request @@ -180,7 +180,7 @@ public static MockHttpServletRequestBuilder options(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. - * + * * @param uri the URL * @return the builder for the OPTIONS request */ @@ -191,7 +191,7 @@ public static MockHttpServletRequestBuilder options(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. The url template * will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the HEAD request @@ -204,7 +204,7 @@ public static MockHttpServletRequestBuilder head(String urlTemplate, /** * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. - * + * * @param uri the URL * @return the builder for the HEAD request */ @@ -215,7 +215,7 @@ public static MockHttpServletRequestBuilder head(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP * method. The url template will be captured and made available for documentation. - * + * * @param httpMethod the HTTP method * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables @@ -241,7 +241,7 @@ public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI u /** * Create a {@link MockHttpServletRequestBuilder} for a multipart request. The url * template will be captured and made available for documentation. - * + * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the file upload request @@ -255,7 +255,7 @@ public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTempla /** * Create a {@link MockHttpServletRequestBuilder} for a multipart request. - * + * * @param uri the URL * @return the builder for the file upload request */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 35d65436f..425cd2fc0 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.mockmvc; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; - import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -35,9 +33,11 @@ import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.Assert; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; + /** * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. - * + * * @author Andy Wilkinson * @author Andreas Evers * @see MockMvcRestDocumentation#document(String, Snippet...) @@ -105,7 +105,7 @@ public void handle(MvcResult result) throws Exception { /** * Adds the given {@code snippets} such that that are documented when this result * handler is called. - * + * * @param snippets the snippets to add * @return this {@code ResultDocumentationResultHandler} */ @@ -123,7 +123,7 @@ private List getSnippets(MvcResult result) { return combinedSnippets; } - static final class IdentityOperationRequestPreprocessor implements + private static final class IdentityOperationRequestPreprocessor implements OperationRequestPreprocessor { @Override @@ -133,7 +133,7 @@ public OperationRequest preprocess(OperationRequest request) { } - static final class IdentityOperationResponsePreprocessor implements + private static final class IdentityOperationResponsePreprocessor implements OperationResponsePreprocessor { @Override diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index ba874f42b..2723d0166 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -27,7 +27,7 @@ /** * A configurer that can be used to configure the generated documentation snippets. - * + * * @author Andy Wilkinson */ public class SnippetConfigurer extends @@ -38,7 +38,8 @@ public class SnippetConfigurer extends HttpDocumentation.httpResponse()); /** - * The default encoding for documentation snippets + * The default encoding for documentation snippets. + * * @see #withEncoding(String) */ public static final String DEFAULT_SNIPPET_ENCODING = "UTF-8"; @@ -52,6 +53,7 @@ public class SnippetConfigurer extends /** * Configures any documentation snippets to be written using the given * {@code encoding}. The default is UTF-8. + * * @param encoding the encoding * @return {@code this} */ @@ -70,7 +72,7 @@ void apply(MockHttpServletRequest request) { /** * Configures the documentation snippets that will be produced by default. - * + * * @param defaultSnippets the default snippets * @return {@code this} */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index c931ea967..b7024fd4c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -19,26 +19,30 @@ import org.springframework.mock.web.MockHttpServletRequest; /** - * A configurer that can be used to configure the documented URIs - * + * A configurer that can be used to configure the documented URIs. + * * @author Andy Wilkinson */ -public class UriConfigurer extends AbstractNestedConfigurer { +public class UriConfigurer extends + AbstractNestedConfigurer { /** - * The default scheme for documented URIs + * The default scheme for documented URIs. + * * @see #withScheme(String) */ public static final String DEFAULT_SCHEME = "http"; /** - * The defalt host for documented URIs + * The defalt host for documented URIs. + * * @see #withHost(String) */ public static final String DEFAULT_HOST = "localhost"; /** - * The default port for documented URIs + * The default port for documented URIs. + * * @see #withPort(int) */ public static final int DEFAULT_PORT = 8080; @@ -49,14 +53,14 @@ public class UriConfigurer extends AbstractNestedConfigurer> foo() { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java index 708e1fe9e..d8f78dd5e 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java @@ -16,12 +16,6 @@ package org.springframework.restdocs.mockmvc; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import java.net.URI; import org.junit.Rule; @@ -33,6 +27,12 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + /** * Tests for {@link RestDocumentationMockMvcConfigurer}. * diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index b77fae0f0..a5bd3159f 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -16,6 +16,16 @@ package org.springframework.restdocs.mockmvc; +import java.net.URI; + +import javax.servlet.ServletContext; + +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -29,20 +39,9 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.request; -import java.net.URI; - -import javax.servlet.ServletContext; - -import org.junit.Test; -import org.springframework.http.HttpMethod; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockServletContext; -import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; - /** - * Tests for {@link RestDocumentationRequestBuilders} - * + * Tests for {@link RestDocumentationRequestBuilders}. + * * @author Andy Wilkinson * */ diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/StubMvcResult.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/StubMvcResult.java deleted file mode 100644 index ace52418d..000000000 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/StubMvcResult.java +++ /dev/null @@ -1,154 +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.mockmvc.test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockServletContext; -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; -import org.springframework.restdocs.snippet.StandardWriterResolver; -import org.springframework.restdocs.snippet.WriterResolver; -import org.springframework.restdocs.templates.StandardTemplateResourceResolver; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.RequestBuilder; -import org.springframework.web.servlet.FlashMap; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -/** - * A minimal stub implementation of {@link MvcResult} - * - * @author Andy Wilkinson - * - */ -public class StubMvcResult implements MvcResult { - - private final MockHttpServletRequest request; - - private final MockHttpServletResponse response; - - public static StubMvcResult result() { - return new StubMvcResult(); - } - - public static StubMvcResult result(RequestBuilder requestBuilder) { - return new StubMvcResult(requestBuilder); - } - - public static StubMvcResult result(RequestBuilder requestBuilder, - MockHttpServletResponse response) { - return new StubMvcResult(requestBuilder, response); - } - - public static StubMvcResult result(MockHttpServletRequest request) { - return new StubMvcResult(request); - } - - public static StubMvcResult result(MockHttpServletResponse response) { - return new StubMvcResult(response); - } - - public static StubMvcResult result(MockHttpServletRequest request, - MockHttpServletResponse response) { - return new StubMvcResult(request, response); - } - - private StubMvcResult() { - this(new MockHttpServletRequest(), new MockHttpServletResponse()); - } - - private StubMvcResult(MockHttpServletRequest request) { - this(request, new MockHttpServletResponse()); - } - - private StubMvcResult(MockHttpServletResponse response) { - this(new MockHttpServletRequest(), response); - } - - private StubMvcResult(RequestBuilder requestBuilder, MockHttpServletResponse response) { - this(requestBuilder.buildRequest(new MockServletContext()), response); - } - - private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { - this.request = request; - if (this.request.getAttribute(TemplateEngine.class.getName()) == null) { - this.request.setAttribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(new StandardTemplateResourceResolver())); - } - RestDocumentationContext context = new RestDocumentationContext(null, null, null); - this.request.setAttribute(RestDocumentationContext.class.getName(), context); - if (this.request.getAttribute(WriterResolver.class.getName()) == null) { - this.request.setAttribute(WriterResolver.class.getName(), - new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context))); - } - this.response = response; - } - - private StubMvcResult(RequestBuilder requestBuilder) { - this(requestBuilder.buildRequest(new MockServletContext())); - } - - @Override - public MockHttpServletRequest getRequest() { - return this.request; - } - - @Override - public MockHttpServletResponse getResponse() { - return this.response; - } - - @Override - public Object getHandler() { - return null; - } - - @Override - public HandlerInterceptor[] getInterceptors() { - return null; - } - - @Override - public ModelAndView getModelAndView() { - return null; - } - - @Override - public Exception getResolvedException() { - return null; - } - - @Override - public FlashMap getFlashMap() { - return null; - } - - @Override - public Object getAsyncResult() { - return null; - } - - @Override - public Object getAsyncResult(long timeToWait) { - return null; - } - -} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/TestRequestBuilders.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/TestRequestBuilders.java deleted file mode 100644 index 0bbc35af6..000000000 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/test/TestRequestBuilders.java +++ /dev/null @@ -1,36 +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.mockmvc.test; - -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; - -public class TestRequestBuilders { - - private TestRequestBuilders() { - - } - - public static MockHttpServletRequestBuilder get(String urlTemplate, - Object... urlVariables) { - return org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get( - urlTemplate, urlVariables).requestAttr( - RestDocumentationContext.class.getName(), - new RestDocumentationContext(null, null, null)); - } - -} From 9b31c0ce9b847df1a2437d20f8190b9abbe8d604 Mon Sep 17 00:00:00 2001 From: MURAKAMI Masahiko Date: Mon, 14 Sep 2015 13:39:00 +0900 Subject: [PATCH 0157/1059] Update HTTP request and response snippets to honour charset of content Closes gh-126 --- .../restdocs/http/HttpRequestSnippet.java | 15 +++++++++-- .../restdocs/http/HttpResponseSnippet.java | 17 +++++++++--- .../restdocs/operation/OperationRequest.java | 9 +++++++ .../restdocs/operation/OperationResponse.java | 10 +++++++ .../operation/StandardOperationRequest.java | 26 ++++++++++++++++++ .../operation/StandardOperationResponse.java | 27 +++++++++++++++++++ .../http/HttpRequestSnippetTests.java | 16 +++++++++++ .../http/HttpResponseSnippetTests.java | 10 ++++++- 8 files changed, 123 insertions(+), 7 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 23bba3a9b..092e1fdb4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.http; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -30,6 +31,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -107,8 +109,7 @@ private String getRequestBody(OperationRequest request) { StringWriter httpRequest = new StringWriter(); PrintWriter writer = new PrintWriter(httpRequest); if (request.getContent().length > 0) { - writer.println(); - writer.print(new String(request.getContent())); + writer.print(requestBody(request)); } else if (isPutOrPost(request)) { if (request.getParts().isEmpty()) { @@ -125,6 +126,16 @@ else if (isPutOrPost(request)) { return httpRequest.toString(); } + private String requestBody(OperationRequest request) { + try { + String content = request.getContentAsString(); + return content.isEmpty() ? content : String.format("%n%s", content); + } + catch (IOException e) { + throw new ModelCreationException("Failed to create response body.", e); + } + } + private boolean isPutOrPost(OperationRequest request) { return HttpMethod.PUT.equals(request.getMethod()) || HttpMethod.POST.equals(request.getMethod()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index e8f659503..69ee312f1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.http; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -25,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -59,16 +61,23 @@ protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); HttpStatus status = response.getStatus(); Map model = new HashMap(); - model.put( - "responseBody", - response.getContent().length > 0 ? String.format("%n%s", new String( - response.getContent())) : ""); + model.put("responseBody", responseBody(response)); model.put("statusCode", status.value()); model.put("statusReason", status.getReasonPhrase()); model.put("headers", headers(response)); return model; } + private String responseBody(OperationResponse response) { + try { + String content = response.getContentAsString(); + return content.isEmpty() ? content : String.format("%n%s", content); + } + catch (IOException e) { + throw new ModelCreationException("Failed to create response body.", e); + } + } + private List> headers(OperationResponse response) { List> headers = new ArrayList<>(); for (Entry> header : response.getHeaders().entrySet()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 052093ce4..a56eae407 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.operation; +import java.io.IOException; import java.net.URI; import java.util.Collection; @@ -38,6 +39,14 @@ public interface OperationRequest { */ byte[] getContent(); + /** + * Returns the contents as string of the request. If the request has no content an empty string + * is returned + * @return the contents as string, never {@code null} + * @throws IOException if an input or output exception occurred + */ + String getContentAsString() throws IOException; + /** * Returns the headers that were included in the request. * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 18485fdb2..4f0cd181b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.operation; +import java.io.IOException; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -49,4 +51,12 @@ public interface OperationResponse { * @return the contents, never {@code null} */ byte[] getContent(); + + /** + * Returns the contents as string of the response. If the response has no content an empty string + * is returned + * @return the contents as string, never {@code null} + * @throws IOException if an input or output exception occurred + */ + String getContentAsString() throws IOException; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 9dfac8c03..f7885e5bc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.operation; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Arrays; import java.util.Collection; @@ -23,6 +24,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; /** * Standard implementation of {@link OperationRequest}. @@ -33,6 +35,8 @@ public class StandardOperationRequest implements OperationRequest { private byte[] content; + private String characterEncoding; + private HttpHeaders headers; private HttpMethod method; @@ -60,6 +64,7 @@ public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, this.uri = uri; this.method = method; this.content = content; + this.characterEncoding = detectCharsetFromContentTypeHeader(headers); this.headers = headers; this.parameters = parameters; this.parts = parts; @@ -69,6 +74,17 @@ public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, public byte[] getContent() { return Arrays.copyOf(this.content, this.content.length); } + + @Override + public String getContentAsString() throws UnsupportedEncodingException { + if (content.length > 0) { + return characterEncoding != null ? + new String(content, characterEncoding) : new String(content); + } + else { + return ""; + } + } @Override public HttpHeaders getHeaders() { @@ -95,4 +111,14 @@ public URI getUri() { return this.uri; } + private String detectCharsetFromContentTypeHeader(HttpHeaders headers) { + if (headers == null) { + return null; + } + MediaType contentType = headers.getContentType(); + if (contentType == null) { + return null; + } + return contentType.getParameter("charset"); + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index faac5f5a9..628e6f19d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -16,8 +16,11 @@ package org.springframework.restdocs.operation; +import java.io.UnsupportedEncodingException; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; /** * Standard implementation of {@link OperationResponse}. @@ -32,6 +35,8 @@ public class StandardOperationResponse implements OperationResponse { private final byte[] content; + private String characterEncoding; + /** * Creates a new response with the given {@code status}, {@code headers}, and * {@code content}. @@ -45,6 +50,7 @@ public StandardOperationResponse(HttpStatus status, HttpHeaders headers, this.status = status; this.headers = headers; this.content = content; + this.characterEncoding = detectCharsetFromContentTypeHeader(headers); } @Override @@ -62,4 +68,25 @@ public byte[] getContent() { return this.content; } + @Override + public String getContentAsString() throws UnsupportedEncodingException { + if (content.length > 0) { + return characterEncoding != null ? + new String(content, characterEncoding) : new String(content); + } + else { + return ""; + } + } + + private String detectCharsetFromContentTypeHeader(HttpHeaders headers) { + if (headers == null) { + return null; + } + MediaType contentType = headers.getContentType(); + if (contentType == null) { + return null; + } + return contentType.getParameter("charset"); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 6dcdcbbe2..e225fa707 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -84,7 +84,23 @@ public void postRequestWithContent() throws IOException { .request("http://localhost/foo").method("POST").content("Hello, world") .build()); } + + @Test + public void postRequestWithCharset() throws IOException { + this.snippet.expectHttpRequest("post-request-with-charset").withContents( + httpRequest(POST, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "text/plain;charset=UTF-8").content( + "こんにちわ, 世界")); // Hello, World in japanese. + new HttpRequestSnippet().document(new OperationBuilder( + "post-request-with-charset", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST") + .header("Content-Type", "text/plain;charset=UTF-8") + .content("こんにちわ, 世界") + .build()); + } + @Test public void postRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("post-request-with-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index ab13d9508..4349f5d0a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -84,7 +84,15 @@ public void responseWithContent() throws IOException { new HttpResponseSnippet().document(new OperationBuilder("response-with-content", this.snippet.getOutputDirectory()).response().content("content").build()); } - + + @Test + public void responseWithCharset() throws IOException { + this.snippet.expectHttpResponse("response-with-charset").withContents( + httpResponse(OK).header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ")); + new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", + this.snippet.getOutputDirectory()).response().header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ").build()); + } + @Test public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( From 3f6d4b96395c8ff7b26b6ba2b61409bf8a73ea0f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 17:05:26 +0100 Subject: [PATCH 0158/1059] Honour Content-Type charset whenever content is turned into a String This commit builds on the changes made in the previous commit to make wider use of the new getContentAsString methods on OperationRequest and OperationResponse. It also adds such a method to OperationRequestPart. Any code that previously got the content as a byte array and then created a String has been updated to either use getContentAsString or to use the charset from the Content-Type header when creating a String. See gh-126 --- .../restdocs/curl/CurlRequestSnippet.java | 8 +-- .../restdocs/http/HttpRequestSnippet.java | 19 ++--- .../restdocs/http/HttpResponseSnippet.java | 11 +-- .../hypermedia/AbstractJsonLinkExtractor.java | 2 +- .../operation/AbstractOperationMessage.java | 69 +++++++++++++++++++ .../restdocs/operation/OperationRequest.java | 15 ++-- .../operation/OperationRequestPart.java | 10 +++ .../restdocs/operation/OperationResponse.java | 15 ++-- .../operation/StandardOperationRequest.java | 47 +------------ .../StandardOperationRequestPart.java | 20 +----- .../operation/StandardOperationResponse.java | 47 +------------ .../operation/preprocess/ContentModifier.java | 4 +- ...ContentModifyingOperationPreprocessor.java | 7 +- .../LinkMaskingContentModifier.java | 6 +- .../PatternReplacingContentModifier.java | 24 +++++-- .../PrettyPrintingContentModifier.java | 4 +- .../http/HttpRequestSnippetTests.java | 15 ++-- .../http/HttpResponseSnippetTests.java | 14 ++-- ...ntModifyingOperationPreprocessorTests.java | 3 +- .../LinkMaskingContentModifierTests.java | 21 +++--- .../PatternReplacingContentModifierTests.java | 20 ++++-- .../PrettyPrintingContentModifierTests.java | 19 ++--- 22 files changed, 202 insertions(+), 198 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 9270c126f..57cddd6c2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -80,7 +80,6 @@ private String getOptions(Operation operation) { writeHttpMethodIfNecessary(operation.getRequest(), printer); writeHeaders(headers, printer); writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); return command.toString(); @@ -126,7 +125,7 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) for (OperationRequestPart part : request.getParts()) { writer.printf(" -F '%s=", part.getName()); if (!StringUtils.hasText(part.getSubmittedFileName())) { - writer.append(new String(part.getContent())); + writer.append(part.getContentAsString()); } else { writer.printf("@%s", part.getSubmittedFileName()); @@ -141,8 +140,9 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) } private void writeContent(OperationRequest request, PrintWriter writer) { - if (request.getContent().length > 0) { - writer.print(String.format(" -d '%s'", new String(request.getContent()))); + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + writer.print(String.format(" -d '%s'", content)); } else if (!request.getParts().isEmpty()) { for (Entry> entry : request.getParameters().entrySet()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 092e1fdb4..9b73fde48 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.http; -import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -31,7 +30,6 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -108,8 +106,9 @@ private List> getHeaders(OperationRequest request) { private String getRequestBody(OperationRequest request) { StringWriter httpRequest = new StringWriter(); PrintWriter writer = new PrintWriter(httpRequest); - if (request.getContent().length > 0) { - writer.print(requestBody(request)); + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + writer.printf("%n%s", content); } else if (isPutOrPost(request)) { if (request.getParts().isEmpty()) { @@ -126,16 +125,6 @@ else if (isPutOrPost(request)) { return httpRequest.toString(); } - private String requestBody(OperationRequest request) { - try { - String content = request.getContentAsString(); - return content.isEmpty() ? content : String.format("%n%s", content); - } - catch (IOException e) { - throw new ModelCreationException("Failed to create response body.", e); - } - } - private boolean isPutOrPost(OperationRequest request) { return HttpMethod.PUT.equals(request.getMethod()) || HttpMethod.POST.equals(request.getMethod()); @@ -163,7 +152,7 @@ private void writePartBoundary(PrintWriter writer) { } private void writePart(OperationRequestPart part, PrintWriter writer) { - writePart(part.getName(), new String(part.getContent()), part.getHeaders() + writePart(part.getName(), part.getContentAsString(), part.getHeaders() .getContentType(), writer); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 69ee312f1..9d2de12c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.http; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,7 +25,6 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -69,13 +67,8 @@ protected Map createModel(Operation operation) { } private String responseBody(OperationResponse response) { - try { - String content = response.getContentAsString(); - return content.isEmpty() ? content : String.format("%n%s", content); - } - catch (IOException e) { - throw new ModelCreationException("Failed to create response body.", e); - } + String content = response.getContentAsString(); + return content.isEmpty() ? content : String.format("%n%s", content); } private List> headers(OperationResponse response) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index 4b952ca2b..cc9f115ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -38,7 +38,7 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor { public Map> extractLinks(OperationResponse response) throws IOException { Map jsonContent = this.objectMapper.readValue( - new String(response.getContent()), Map.class); + response.getContent(), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java new file mode 100644 index 000000000..c5ede617d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -0,0 +1,69 @@ +/* + * 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.operation; + +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +/** + * Abstract base class for operation requests, request parts, and responses. + * + * @author Andy Wilkinson + */ +abstract class AbstractOperationMessage { + + private final byte[] content; + + private final HttpHeaders headers; + + AbstractOperationMessage(byte[] content, HttpHeaders headers) { + this.content = content == null ? new byte[0] : content; + this.headers = headers; + } + + public byte[] getContent() { + return Arrays.copyOf(this.content, this.content.length); + } + + public HttpHeaders getHeaders() { + return HttpHeaders.readOnlyHttpHeaders(this.headers); + } + + public String getContentAsString() { + if (this.content.length > 0) { + Charset charset = extractCharsetFromContentTypeHeader(); + return charset != null ? new String(this.content, charset) : new String( + this.content); + } + return ""; + } + + private Charset extractCharsetFromContentTypeHeader() { + if (this.headers == null) { + return null; + } + MediaType contentType = this.headers.getContentType(); + if (contentType == null) { + return null; + } + return contentType.getCharSet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index a56eae407..29153eec6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.operation; -import java.io.IOException; import java.net.URI; import java.util.Collection; @@ -32,20 +31,22 @@ public interface OperationRequest { /** - * Returns the contents of the request. If the request has no content an empty array - * is returned. + * Returns the content of the request. If the request has no content an empty array is + * returned. * * @return the contents, never {@code null} */ byte[] getContent(); /** - * Returns the contents as string of the request. If the request has no content an empty string - * is returned + * Returns the content of the request as a {@link String}. If the request has no + * content an empty string is returned. If the request has a {@code Content-Type} + * header that specifies a charset then that charset will be used when converting the + * contents to a {@code String}. + * * @return the contents as string, never {@code null} - * @throws IOException if an input or output exception occurred */ - String getContentAsString() throws IOException; + String getContentAsString(); /** * Returns the headers that were included in the request. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java index 69a6feca3..13cb8ed06 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java @@ -47,6 +47,16 @@ public interface OperationRequestPart { */ byte[] getContent(); + /** + * Returns the content of the part as a {@link String}. If the part has no content an + * empty string is returned. If the part has a {@code Content-Type} header that + * specifies a charset then that charset will be used when converting the contents to + * a {@code String}. + * + * @return the contents as string, never {@code null} + */ + String getContentAsString(); + /** * Returns the part's headers. * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 4f0cd181b..6a5186791 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.operation; -import java.io.IOException; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -45,7 +43,7 @@ public interface OperationResponse { HttpHeaders getHeaders(); /** - * Returns the contents of the response. If the response has no content an empty array + * Returns the content of the response. If the response has no content an empty array * is returned. * * @return the contents, never {@code null} @@ -53,10 +51,13 @@ public interface OperationResponse { byte[] getContent(); /** - * Returns the contents as string of the response. If the response has no content an empty string - * is returned + * Returns the content of the response as a {@link String}. If the response has no + * content an empty string is returned. If the response has a {@code Content-Type} + * header that specifies a charset then that charset will be used when converting the + * contents to a {@code String}. + * * @return the contents as string, never {@code null} - * @throws IOException if an input or output exception occurred */ - String getContentAsString() throws IOException; + String getContentAsString(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index f7885e5bc..c869100b4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -16,28 +16,20 @@ package org.springframework.restdocs.operation; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; /** * Standard implementation of {@link OperationRequest}. * * @author Andy Wilkinson */ -public class StandardOperationRequest implements OperationRequest { - - private byte[] content; - - private String characterEncoding; - - private HttpHeaders headers; +public class StandardOperationRequest extends AbstractOperationMessage implements + OperationRequest { private HttpMethod method; @@ -61,36 +53,13 @@ public class StandardOperationRequest implements OperationRequest { public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { + super(content, headers); this.uri = uri; this.method = method; - this.content = content; - this.characterEncoding = detectCharsetFromContentTypeHeader(headers); - this.headers = headers; this.parameters = parameters; this.parts = parts; } - @Override - public byte[] getContent() { - return Arrays.copyOf(this.content, this.content.length); - } - - @Override - public String getContentAsString() throws UnsupportedEncodingException { - if (content.length > 0) { - return characterEncoding != null ? - new String(content, characterEncoding) : new String(content); - } - else { - return ""; - } - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - @Override public HttpMethod getMethod() { return this.method; @@ -111,14 +80,4 @@ public URI getUri() { return this.uri; } - private String detectCharsetFromContentTypeHeader(HttpHeaders headers) { - if (headers == null) { - return null; - } - MediaType contentType = headers.getContentType(); - if (contentType == null) { - return null; - } - return contentType.getParameter("charset"); - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index b6c7bd034..c0f6e8917 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -23,16 +23,13 @@ * * @author Andy Wilkinson */ -public class StandardOperationRequestPart implements OperationRequestPart { +public class StandardOperationRequestPart extends AbstractOperationMessage implements + OperationRequestPart { private final String name; private final String submittedFileName; - private final byte[] content; - - private final HttpHeaders headers; - /** * Creates a new {@code StandardOperationRequestPart} with the given {@code name}. * @@ -43,10 +40,9 @@ public class StandardOperationRequestPart implements OperationRequestPart { */ public StandardOperationRequestPart(String name, String submittedFileName, byte[] content, HttpHeaders headers) { + super(content, headers); this.name = name; this.submittedFileName = submittedFileName; - this.content = content; - this.headers = headers; } @Override @@ -59,14 +55,4 @@ public String getSubmittedFileName() { return this.submittedFileName; } - @Override - public byte[] getContent() { - return this.content; - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 628e6f19d..1e42beb20 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -16,27 +16,19 @@ package org.springframework.restdocs.operation; -import java.io.UnsupportedEncodingException; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; /** * Standard implementation of {@link OperationResponse}. * * @author Andy Wilkinson */ -public class StandardOperationResponse implements OperationResponse { +public class StandardOperationResponse extends AbstractOperationMessage implements + OperationResponse { private final HttpStatus status; - private final HttpHeaders headers; - - private final byte[] content; - - private String characterEncoding; - /** * Creates a new response with the given {@code status}, {@code headers}, and * {@code content}. @@ -47,10 +39,8 @@ public class StandardOperationResponse implements OperationResponse { */ public StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) { + super(content, headers); this.status = status; - this.headers = headers; - this.content = content; - this.characterEncoding = detectCharsetFromContentTypeHeader(headers); } @Override @@ -58,35 +48,4 @@ public HttpStatus getStatus() { return this.status; } - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public byte[] getContent() { - return this.content; - } - - @Override - public String getContentAsString() throws UnsupportedEncodingException { - if (content.length > 0) { - return characterEncoding != null ? - new String(content, characterEncoding) : new String(content); - } - else { - return ""; - } - } - - private String detectCharsetFromContentTypeHeader(HttpHeaders headers) { - if (headers == null) { - return null; - } - MediaType contentType = headers.getContentType(); - if (contentType == null) { - return null; - } - return contentType.getParameter("charset"); - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java index 642c00278..8781a817f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.operation.preprocess; +import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -33,8 +34,9 @@ public interface ContentModifier { * Returns modified content based on the given {@code originalContent}. * * @param originalContent the original content + * @param contentType the type of the original content, may be {@code null} * @return the modified content */ - byte[] modifyContent(byte[] originalContent); + byte[] modifyContent(byte[] originalContent, MediaType contentType); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 18f5602ad..ad37baea1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -44,7 +44,8 @@ public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { @Override public OperationRequest preprocess(OperationRequest request) { - byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent()); + byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent(), + request.getHeaders().getContentType()); return new StandardOperationRequest(request.getUri(), request.getMethod(), modifiedContent, getUpdatedHeaders(request.getHeaders(), modifiedContent), @@ -53,8 +54,8 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { - byte[] modifiedContent = this.contentModifier - .modifyContent(response.getContent()); + byte[] modifiedContent = this.contentModifier.modifyContent( + response.getContent(), response.getHeaders().getContentType()); return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders( response.getHeaders(), modifiedContent), modifiedContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 1a2b56cd3..9af9c03f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -18,6 +18,8 @@ import java.util.regex.Pattern; +import org.springframework.http.MediaType; + /** * A content modifier the masks the {@code href} of any hypermedia links. * @@ -41,8 +43,8 @@ class LinkMaskingContentModifier implements ContentModifier { } @Override - public byte[] modifyContent(byte[] originalContent) { - return this.contentModifier.modifyContent(originalContent); + public byte[] modifyContent(byte[] originalContent, MediaType contentType) { + return this.contentModifier.modifyContent(originalContent, contentType); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 81464c91b..58d4e3a7a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -19,6 +19,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.http.MediaType; + /** * A {@link ContentModifier} that modifies the content by replacing occurrences of a * regular expression {@link Pattern}. @@ -45,15 +47,29 @@ class PatternReplacingContentModifier implements ContentModifier { } @Override - public byte[] modifyContent(byte[] content) { - String original = new String(content); + public byte[] modifyContent(byte[] content, MediaType contentType) { + String original; + if (contentType != null && contentType.getCharSet() != null) { + original = new String(content, contentType.getCharSet()); + } + else { + original = new String(content); + } Matcher matcher = this.pattern.matcher(original); StringBuilder buffer = new StringBuilder(); int previous = 0; while (matcher.find()) { - buffer.append(original.substring(previous, matcher.start(1))); + String prefix; + if (matcher.groupCount() > 0) { + prefix = original.substring(previous, matcher.start(1)); + previous = matcher.end(1); + } + else { + prefix = original.substring(previous, matcher.start()); + previous = matcher.end(); + } + buffer.append(prefix); buffer.append(this.replacement); - previous = matcher.end(1); } if (previous < original.length()) { buffer.append(original.substring(previous)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 6902e1211..49af2bf46 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -29,6 +29,8 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.springframework.http.MediaType; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -44,7 +46,7 @@ public class PrettyPrintingContentModifier implements ContentModifier { new XmlPrettyPrinter())); @Override - public byte[] modifyContent(byte[] originalContent) { + public byte[] modifyContent(byte[] originalContent, MediaType contentType) { for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { try { return prettyPrinter.prettyPrint(originalContent).getBytes(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index e225fa707..55cc9a07c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -84,23 +84,22 @@ public void postRequestWithContent() throws IOException { .request("http://localhost/foo").method("POST").content("Hello, world") .build()); } - + @Test public void postRequestWithCharset() throws IOException { this.snippet.expectHttpRequest("post-request-with-charset").withContents( - httpRequest(POST, "/foo") - .header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "text/plain;charset=UTF-8").content( - "こんにちわ, 世界")); // Hello, World in japanese. + httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "text/plain;charset=UTF-8") + .content("こんにちわ, 世界")); // Hello, World in Japanese new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-charset", this.snippet.getOutputDirectory()) .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8") - .content("こんにちわ, 世界") + .header("Content-Type", "text/plain;charset=UTF-8").content("こんにちわ, 世界") .build()); } - + @Test public void postRequestWithParameter() throws IOException { this.snippet.expectHttpRequest("post-request-with-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 4349f5d0a..cd6650f02 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -68,8 +68,7 @@ public void nonOkResponse() throws IOException { @Test public void responseWithHeaders() throws IOException { this.snippet.expectHttpResponse("response-with-headers").withContents( - httpResponse(HttpStatus.OK) // - .header("Content-Type", "application/json") // + httpResponse(HttpStatus.OK).header("Content-Type", "application/json") .header("a", "alpha")); new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", this.snippet.getOutputDirectory()).response() @@ -84,15 +83,18 @@ public void responseWithContent() throws IOException { new HttpResponseSnippet().document(new OperationBuilder("response-with-content", this.snippet.getOutputDirectory()).response().content("content").build()); } - + @Test public void responseWithCharset() throws IOException { this.snippet.expectHttpResponse("response-with-charset").withContents( - httpResponse(OK).header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ")); + httpResponse(HttpStatus.OK).header("Content-Type", + "text/plain;charset=UTF-8").content("コンテンツ")); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", - this.snippet.getOutputDirectory()).response().header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ").build()); + this.snippet.getOutputDirectory()).response() + .header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ") + .build()); } - + @Test public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 6923b7dbf..5a38b1bde 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -23,6 +23,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; @@ -46,7 +47,7 @@ public class ContentModifyingOperationPreprocessorTests { new ContentModifier() { @Override - public byte[] modifyContent(byte[] originalContent) { + public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { return "modified".getBytes(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index e63b887c3..2dfdf64b2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -52,37 +52,36 @@ public class LinkMaskingContentModifierTests { @Test public void halLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent(halPayloadWithLinks(this.links)), + assertThat( + this.contentModifier.modifyContent(halPayloadWithLinks(this.links), null), is(equalTo(halPayloadWithLinks(this.maskedLinks)))); } @Test public void formattedHalLinksAreMasked() throws Exception { - assertThat( - this.contentModifier - .modifyContent(formattedHalPayloadWithLinks(this.links)), + assertThat(this.contentModifier.modifyContent( + formattedHalPayloadWithLinks(this.links), null), is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); } @Test public void atomLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links)), - is(equalTo(atomPayloadWithLinks(this.maskedLinks)))); + assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links), + null), is(equalTo(atomPayloadWithLinks(this.maskedLinks)))); } @Test public void formattedAtomLinksAreMasked() throws Exception { - assertThat( - this.contentModifier - .modifyContent(formattedAtomPayloadWithLinks(this.links)), + assertThat(this.contentModifier.modifyContent( + formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); } @Test public void maskCanBeCustomized() throws Exception { assertThat( - new LinkMaskingContentModifier("custom") - .modifyContent(formattedAtomPayloadWithLinks(this.links)), + new LinkMaskingContentModifier("custom").modifyContent( + formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index de9ba056f..8fedbc3d9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -16,9 +16,11 @@ package org.springframework.restdocs.operation.preprocess; +import java.nio.charset.Charset; import java.util.regex.Pattern; import org.junit.Test; +import org.springframework.http.MediaType; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -35,13 +37,23 @@ public class PatternReplacingContentModifierTests { @Test public void patternsAreReplaced() throws Exception { Pattern pattern = Pattern.compile( - "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( pattern, "<>"); - assertThat( - contentModifier.modifyContent("{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}" - .getBytes()), is(equalTo("{\"id\" : \"<>\"}".getBytes()))); + assertThat(contentModifier.modifyContent( + "{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}".getBytes(), null), + is(equalTo("{\"id\" : \"<>\"}".getBytes()))); + } + + @Test + public void encodingIsPreserved() { + Pattern pattern = Pattern.compile("[0-9]+"); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( + pattern, "<>"); + assertThat(contentModifier.modifyContent("こんにちわ, 世界 123".getBytes(), + new MediaType("text", "plain", Charset.forName("UTF-8"))), + is(equalTo("こんにちわ, 世界 <>".getBytes()))); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index abedadf30..85e5edf9a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -31,16 +31,16 @@ public class PrettyPrintingContentModifierTests { @Test public void prettyPrintJson() throws Exception { - assertThat( - new PrettyPrintingContentModifier().modifyContent("{\"a\":5}".getBytes()), - equalTo(String.format("{%n \"a\" : 5%n}").getBytes())); + assertThat(new PrettyPrintingContentModifier().modifyContent( + "{\"a\":5}".getBytes(), null), equalTo(String.format("{%n \"a\" : 5%n}") + .getBytes())); } @Test public void prettyPrintXml() throws Exception { - assertThat( - new PrettyPrintingContentModifier().modifyContent("" - .getBytes()), equalTo(String.format( + assertThat(new PrettyPrintingContentModifier().modifyContent( + "".getBytes(), null), + equalTo(String.format( "%n" + "%n %n%n") .getBytes())); @@ -48,14 +48,15 @@ public void prettyPrintXml() throws Exception { @Test public void empytContentIsHandledGracefully() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes()), + assertThat( + new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), equalTo("".getBytes())); } @Test public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { String content = "abcdefg"; - assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes()), - equalTo(content.getBytes())); + assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), + null), equalTo(content.getBytes())); } } From f8cbb3b63c948df20064eec8dec7d9dcf5f58717 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 20:51:44 +0100 Subject: [PATCH 0159/1059] Avoid using UTF-8 characters in source code Using UTF-8 in the source can be problematic on platforms that are not configured to use UTF-8 by default. This commit replaces a number of strings with UTF-8 characters in them with an equivalent ASCII string that uses unicode character escapes. --- .../restdocs/http/HttpRequestSnippetTests.java | 7 ++++--- .../restdocs/http/HttpResponseSnippetTests.java | 7 ++++--- .../preprocess/PatternReplacingContentModifierTests.java | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 55cc9a07c..bd93a77cb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -87,17 +87,18 @@ public void postRequestWithContent() throws IOException { @Test public void postRequestWithCharset() throws IOException { + String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; this.snippet.expectHttpRequest("post-request-with-charset").withContents( httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "text/plain;charset=UTF-8") - .content("こんにちわ, 世界")); // Hello, World in Japanese + .content(japaneseContent)); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-charset", this.snippet.getOutputDirectory()) .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8").content("こんにちわ, 世界") - .build()); + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index cd6650f02..c40dff68c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -86,13 +86,14 @@ public void responseWithContent() throws IOException { @Test public void responseWithCharset() throws IOException { + String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; this.snippet.expectHttpResponse("response-with-charset").withContents( httpResponse(HttpStatus.OK).header("Content-Type", - "text/plain;charset=UTF-8").content("コンテンツ")); + "text/plain;charset=UTF-8").content(japaneseContent)); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() - .header("Content-Type", "text/plain;charset=UTF-8").content("コンテンツ") - .build()); + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 8fedbc3d9..1046cad9a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -48,12 +48,13 @@ public void patternsAreReplaced() throws Exception { @Test public void encodingIsPreserved() { + String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; Pattern pattern = Pattern.compile("[0-9]+"); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( pattern, "<>"); - assertThat(contentModifier.modifyContent("こんにちわ, 世界 123".getBytes(), + assertThat(contentModifier.modifyContent((japaneseContent + " 123").getBytes(), new MediaType("text", "plain", Charset.forName("UTF-8"))), - is(equalTo("こんにちわ, 世界 <>".getBytes()))); + is(equalTo((japaneseContent + " <>").getBytes()))); } } From 8f9c4249af46d91c5458319655c151a0a1df05f6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 21:32:52 +0100 Subject: [PATCH 0160/1059] Use UTF-8 when reading in snippet files to verify their contents --- .../org/springframework/restdocs/test/SnippetMatchers.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 8bbb6de37..f0d54a812 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -17,8 +17,9 @@ package org.springframework.restdocs.test; import java.io.File; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; @@ -251,7 +252,8 @@ private boolean snippetFileExists(Object item) { } private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString(new FileReader(snippetFile)); + return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream( + snippetFile), "UTF-8")); } @Override From cc0edcb9017f07c883cd2af506ffd4185c0a40aa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Sep 2015 22:19:45 +0100 Subject: [PATCH 0161/1059] =?UTF-8?q?Avoid=20using=20JVM=E2=80=99s=20defau?= =?UTF-8?q?lt=20charset=20for=20String=20to=20byte[]=20in=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../restdocs/http/HttpRequestSnippetTests.java | 2 +- .../restdocs/http/HttpResponseSnippetTests.java | 2 +- .../org/springframework/restdocs/test/OperationBuilder.java | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index bd93a77cb..83ce80ba9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -98,7 +98,7 @@ public void postRequestWithCharset() throws IOException { "post-request-with-charset", this.snippet.getOutputDirectory()) .request("http://localhost/foo").method("POST") .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent).build()); + .content(japaneseContent.getBytes("UTF-8")).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index c40dff68c..b2a259401 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -93,7 +93,7 @@ public void responseWithCharset() throws IOException { new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent).build()); + .content(japaneseContent.getBytes("UTF-8")).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 50d6cd83c..51d66504a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -140,6 +140,11 @@ public OperationRequestBuilder content(String content) { return this; } + public OperationRequestBuilder content(byte[] content) { + this.content = content; + return this; + } + public OperationRequestBuilder param(String name, String... values) { for (String value : values) { this.parameters.add(name, value); From 5da4bee3c6f281398c229fdca06541ec07e0cc8a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 11:14:58 +0100 Subject: [PATCH 0162/1059] Improve handling of the Content-Length header Previously, some MockMvc-specific logic would add a Content-Length header to every request that had content. This led to the curl request snippet containing a -H option for the Content-Length header. This is unnecessary as curl will automatically generate a Content-Length header based on the data that's being sent to the server. A secondary problem was the inconsistent automatic addition of a Content-Length header; the header was not automatically added to responses. This commit remove the MockMvc-specific logic in favour of some new logic in the core project to automatically add a Content-Length header to both requests and responses. The curl request snippet has been updated to supress the header in favour of curl's automatic generation. Closes gh-111 --- .../restdocs/curl/CurlRequestSnippet.java | 6 ++-- .../operation/AbstractOperationMessage.java | 13 ++++++- ...ContentModifyingOperationPreprocessor.java | 5 ++- .../http/HttpRequestSnippetTests.java | 24 +++++++------ .../http/HttpResponseSnippetTests.java | 17 +++++---- ...ntModifyingOperationPreprocessorTests.java | 10 ------ .../restdocs/test/SnippetMatchers.java | 6 ++++ .../RestDocumentationMockMvcConfigurer.java | 17 +-------- .../MockMvcOperationRequestFactoryTests.java | 3 +- ...kMvcRestDocumentationIntegrationTests.java | 35 ++++++++++++++----- .../RestDocumentationConfigurerTests.java | 12 ------- 11 files changed, 81 insertions(+), 67 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 57cddd6c2..4d1ce4461 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -115,8 +115,10 @@ private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter wr private void writeHeaders(HttpHeaders headers, PrintWriter writer) { for (Entry> entry : headers.entrySet()) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey())) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index c5ede617d..d111f321b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -35,7 +35,18 @@ abstract class AbstractOperationMessage { AbstractOperationMessage(byte[] content, HttpHeaders headers) { this.content = content == null ? new byte[0] : content; - this.headers = headers; + this.headers = createHeaders(content, headers); + } + + private static HttpHeaders createHeaders(byte[] content, HttpHeaders input) { + HttpHeaders headers = new HttpHeaders(); + if (input != null) { + headers.putAll(input); + } + if (content != null && content.length > 0 && headers.getContentLength() == -1) { + headers.setContentLength(content.length); + } + return headers; } public byte[] getContent() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index ad37baea1..5cc31561b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -63,9 +63,12 @@ public OperationResponse preprocess(OperationResponse response) { private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) { HttpHeaders updatedHeaders = new HttpHeaders(); updatedHeaders.putAll(headers); - if (updatedHeaders.getContentLength() > -1) { + if (updatedContent.length > 0) { updatedHeaders.setContentLength(updatedContent.length); } + else { + updatedHeaders.remove(HttpHeaders.CONTENT_LENGTH); + } return updatedHeaders; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 83ce80ba9..9ccb89f1d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -75,30 +75,33 @@ public void getRequestWithQueryString() throws IOException { @Test public void postRequestWithContent() throws IOException { + String content = "Hello, world"; this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, - "localhost").content("Hello, world")); + httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").content("Hello, world") - .build()); + .request("http://localhost/foo").method("POST").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; + byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpRequest("post-request-with-charset").withContents( httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-charset", this.snippet.getOutputDirectory()) .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent.getBytes("UTF-8")).build()); + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test @@ -117,14 +120,15 @@ public void postRequestWithParameter() throws IOException { @Test public void putRequestWithContent() throws IOException { + String content = "Hello, world"; this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(RequestMethod.PUT, "/foo").header(HttpHeaders.HOST, - "localhost").content("Hello, world")); + httpRequest(RequestMethod.PUT, "/foo") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder( "put-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").content("Hello, world") - .build()); + .request("http://localhost/foo").method("PUT").content(content).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index b2a259401..4959e7774 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -78,22 +78,27 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { + String content = "content"; this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(HttpStatus.OK).content("content")); + httpResponse(HttpStatus.OK).content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-content", - this.snippet.getOutputDirectory()).response().content("content").build()); + this.snippet.getOutputDirectory()).response().content(content).build()); } @Test public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; + byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpResponse("response-with-charset").withContents( - httpResponse(HttpStatus.OK).header("Content-Type", - "text/plain;charset=UTF-8").content(japaneseContent)); + httpResponse(HttpStatus.OK) + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent) + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() - .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent.getBytes("UTF-8")).build()); + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 5a38b1bde..2f38bbb36 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -71,16 +71,6 @@ public void modifyResponseContent() { assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); } - @Test - public void unknownContentLengthIsUnchanged() { - StandardOperationRequest request = new StandardOperationRequest( - URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), - new HttpHeaders(), new Parameters(), - Collections.emptyList()); - OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(-1L))); - } - @Test public void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index f0d54a812..d45388a09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -164,6 +164,12 @@ public T header(String name, String value) { return (T) this; } + @SuppressWarnings("unchecked") + public T header(String name, long value) { + this.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 0fd6d7caa..536f831d4 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -29,7 +29,6 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; /** @@ -62,8 +61,7 @@ public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter RestDocumentationMockMvcConfigurer(RestDocumentation restDocumentation) { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( restDocumentation, this.uriConfigurer, this.writerResolverConfigurer, - this.snippetConfigurer, new ContentLengthHeaderConfigurer(), - this.templateEngineConfigurer); + this.snippetConfigurer, this.templateEngineConfigurer); } /** @@ -115,19 +113,6 @@ public RequestPostProcessor beforeMockMvcCreated( return this.requestPostProcessor; } - private static final class ContentLengthHeaderConfigurer extends AbstractConfigurer { - - @Override - void apply(MockHttpServletRequest request) { - long contentLength = request.getContentLengthLong(); - if (contentLength > 0 - && !StringUtils.hasText(request.getHeader("Content-Length"))) { - request.addHeader("Content-Length", request.getContentLengthLong()); - } - } - - } - private static final class TemplateEngineConfigurer extends AbstractConfigurer { private TemplateEngine templateEngine = new MustacheTemplateEngine( diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java index 856405f5b..6770d31bb 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java @@ -157,7 +157,8 @@ public void mockMultipartFileUpload() throws Exception { OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("file"))); assertThat(part.getSubmittedFileName(), is(nullValue())); - assertThat(part.getHeaders().isEmpty(), is(true)); + assertThat(part.getHeaders().size(), is(1)); + assertThat(part.getHeaders().getContentLength(), is(4L)); assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 10985bea9..ad6f34539 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -79,6 +79,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -120,6 +121,21 @@ public void basicSnippetGeneration() throws Exception { "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } + @Test + public void curlSnippetWithContent() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) + .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); + assertThat(new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + "'http://localhost:8080/' -i -X POST " + + "-H 'Accept: application/json' -d 'content'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -298,24 +314,27 @@ public void preprocessedResponse() throws Exception { removeHeaders("a"), replacePattern(pattern, "\"<>\"")))); + String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), is(snippet().withContents( httpResponse(HttpStatus.OK) .header("a", "alpha") .header("Content-Type", "application/json") - .content( - "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," - + "\"href\":\"href\"}]}")))); + .header(HttpHeaders.CONTENT_LENGTH, + original.getBytes().length).content(original)))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), is(snippet().withContents( - httpResponse(HttpStatus.OK).header("Content-Type", - "application/json").content( - String.format("{%n \"a\" : \"<>\",%n \"links\" :" - + " [ {%n \"rel\" : \"rel\",%n \"href\" :" - + " \"...\"%n } ]%n}"))))); + httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); } @Test diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java index d8f78dd5e..486bfd8e6 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java @@ -27,7 +27,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; @@ -94,17 +93,6 @@ public void noContentLengthHeaderWhenRequestHasNotContent() { assertThat(this.request.getHeader("Content-Length"), is(nullValue())); } - @Test - public void contentLengthHeaderIsSetWhenRequestHasContent() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).beforeMockMvcCreated(null, null); - byte[] content = "Hello, world".getBytes(); - this.request.setContent(content); - postProcessor.postProcessRequest(this.request); - assertThat(this.request.getHeader("Content-Length"), - is(equalTo(Integer.toString(content.length)))); - } - private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); From 535bea24f915e87892d213eaa3f0223dddef0392 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 15:16:20 +0100 Subject: [PATCH 0163/1059] Improve handling of Host header so a preprocessor can remove it Previously, the Host header was treated specially in the HTTP request snippet. If no Host header was specified, one would always be added prior to producing the snippet. This ensured that the snippet was valid (an HTTP 1.1 request must include a Host header), but came at the cost of some confusion about why a preprocessor could not remove it. This commit updates the special treatment of the Host header so that it's now performed in a central location so that all of the snippets can benefit from a Host header being added if one isn't provided. The handling of the Content-Length header has also been reworked so that it's performed in the same location. The curl request snippet has been updated so that it doesn't include setting the Host header on the command line; it's unnecessary as curl will automatically include a Host header in the request. The documentation of the Host header's special treatment (made in 2fc0420) that noted that it was unaffected by preprocessing has been reverted. Closes gh-134 --- .../customizing-requests-and-responses.adoc | 4 - .../docs/asciidoc/documenting-your-api.adoc | 3 +- .../restdocs/curl/CurlRequestSnippet.java | 86 +++++++++++++---- .../restdocs/http/HttpRequestSnippet.java | 7 -- .../operation/AbstractOperationMessage.java | 13 +-- .../restdocs/operation/HttpHeadersHelper.java | 66 +++++++++++++ .../operation/OperationRequestFactory.java | 96 +++++++++++++++++++ .../OperationRequestPartFactory.java | 49 ++++++++++ .../operation/OperationResponseFactory.java | 85 ++++++++++++++++ .../operation/StandardOperationRequest.java | 4 +- .../StandardOperationRequestPart.java | 6 +- .../operation/StandardOperationResponse.java | 5 +- ...ContentModifyingOperationPreprocessor.java | 29 ++---- .../HeaderRemovingOperationPreprocessor.java | 17 ++-- .../http/HttpRequestSnippetTests.java | 15 ++- .../ContentTypeLinkExtractorTests.java | 11 ++- .../LinkExtractorsPayloadTests.java | 6 +- ...ntModifyingOperationPreprocessorTests.java | 14 ++- ...derRemovingOperationPreprocessorTests.java | 16 +++- .../restdocs/test/OperationBuilder.java | 13 +-- .../MockMvcOperationRequestFactory.java | 28 +++--- .../MockMvcOperationResponseFactory.java | 6 +- ...kMvcRestDocumentationIntegrationTests.java | 26 +++-- 23 files changed, 466 insertions(+), 139 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index d88fb06d6..f88a58795 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -77,10 +77,6 @@ different replacement can also be specified if you wish. `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. -NOTE: For an HTTP 1.1 request to be valid it must contain a `Host` header. Therefore, -irrespective of any preprocessing, the default HTTP request snippet will always contain a -`Host` header. - [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index e0e477f34..7fd84cdf3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -388,8 +388,7 @@ call that is being documented | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being - documented. HTTP 1.1 requires a `Host` header. If you do not provide one via the - `MockMvc` API the snippet will add one automatically. +documented | `http-response.adoc` | Contains the HTTP response that was returned diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4d1ce4461..73829f249 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -18,10 +18,13 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -43,6 +46,15 @@ */ public class CurlRequestSnippet extends TemplatedSnippet { + private static final Set HEADER_FILTERS; + static { + Set headerFilters = new HashSet(); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); + headerFilters.add(new BasicAuthHeaderFilter()); + HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); + } + /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. */ @@ -76,9 +88,9 @@ private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeIncludeHeadersInOutputOption(printer); - HttpHeaders headers = writeUserOptionIfNecessary(operation.getRequest(), printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(headers, printer); + writeHeaders(operation.getRequest().getHeaders(), printer); writePartsIfNecessary(operation.getRequest(), printer); writeContent(operation.getRequest(), printer); @@ -89,22 +101,12 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private HttpHeaders writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - HttpHeaders headers = new HttpHeaders(); - headers.putAll(request.getHeaders()); - String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); - if (isAuthorizationBasicHeader(authorization)) { - String credentials = new String(Base64Utils.decodeFromString(authorization - .substring(5).trim())); + private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { + String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); writer.print(String.format(" -u '%s'", credentials)); - headers.remove(HttpHeaders.AUTHORIZATION); } - return headers; - } - - private boolean isAuthorizationBasicHeader(String header) { - return header != null && header.startsWith("Basic"); } private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { @@ -115,7 +117,7 @@ private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter wr private void writeHeaders(HttpHeaders headers, PrintWriter writer) { for (Entry> entry : headers.entrySet()) { - if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey())) { + if (allowedHeader(entry)) { for (String header : entry.getValue()) { writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } @@ -123,6 +125,15 @@ private void writeHeaders(HttpHeaders headers, PrintWriter writer) { } } + private boolean allowedHeader(Entry> header) { + for (HeaderFilter headerFilter : HEADER_FILTERS) { + if (!headerFilter.allow(header.getKey(), header.getValue())) { + return false; + } + } + return true; + } + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { for (OperationRequestPart part : request.getParts()) { writer.printf(" -F '%s=", part.getName()); @@ -166,4 +177,45 @@ private boolean isPutOrPost(OperationRequest request) { || HttpMethod.POST.equals(request.getMethod()); } + private interface HeaderFilter { + + boolean allow(String name, List value); + } + + private static final class BasicAuthHeaderFilter implements HeaderFilter { + + @Override + public boolean allow(String name, List value) { + if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { + return false; + } + return true; + } + + static boolean isBasicAuthHeader(List value) { + return value != null && (!value.isEmpty()) + && value.get(0).startsWith("Basic "); + } + + static String decodeBasicAuthHeader(List value) { + return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + } + + } + + private static final class NamedHeaderFilter implements HeaderFilter { + + private final String name; + + private NamedHeaderFilter(String name) { + this.name = name; + } + + @Override + public boolean allow(String name, List value) { + return !this.name.equalsIgnoreCase(name); + } + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 9b73fde48..9060ada91 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -79,9 +79,6 @@ protected Map createModel(Operation operation) { private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); - if (requiresHostHeader(request)) { - headers.add(header(HttpHeaders.HOST, request.getUri().getHost())); - } for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { @@ -170,10 +167,6 @@ private void writeMultipartEnd(PrintWriter writer) { writer.printf("--%s--", MULTIPART_BOUNDARY); } - private boolean requiresHostHeader(OperationRequest request) { - return request.getHeaders().get(HttpHeaders.HOST) == null; - } - private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null && isPutOrPost(request) && !request.getParameters().isEmpty(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index d111f321b..c5ede617d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -35,18 +35,7 @@ abstract class AbstractOperationMessage { AbstractOperationMessage(byte[] content, HttpHeaders headers) { this.content = content == null ? new byte[0] : content; - this.headers = createHeaders(content, headers); - } - - private static HttpHeaders createHeaders(byte[] content, HttpHeaders input) { - HttpHeaders headers = new HttpHeaders(); - if (input != null) { - headers.putAll(input); - } - if (content != null && content.length > 0 && headers.getContentLength() == -1) { - headers.setContentLength(content.length); - } - return headers; + this.headers = headers; } public byte[] getContent() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java new file mode 100644 index 000000000..a9cb7c5a0 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java @@ -0,0 +1,66 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * Helper for working with {@link HttpHeaders}. + * + * @author Andy Wilkinson + */ +class HttpHeadersHelper { + + private final HttpHeaders httpHeaders; + + HttpHeadersHelper(HttpHeaders httpHeaders) { + HttpHeaders headers = new HttpHeaders(); + if (httpHeaders != null) { + headers.putAll(httpHeaders); + } + this.httpHeaders = headers; + } + + HttpHeadersHelper addIfAbsent(String name, String value) { + if (this.httpHeaders.get(name) == null) { + this.httpHeaders.add(name, value); + } + return this; + } + + HttpHeadersHelper updateContentLengthHeaderIfPresent(byte[] content) { + if (this.httpHeaders.getContentLength() != -1) { + setContentLengthHeader(content); + } + return this; + } + + HttpHeadersHelper setContentLengthHeader(byte[] content) { + if (content == null || content.length == 0) { + this.httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); + } + else { + this.httpHeaders.setContentLength(content.length); + } + return this; + } + + HttpHeaders getHeaders() { + return HttpHeaders.readOnlyHttpHeaders(this.httpHeaders); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java new file mode 100644 index 000000000..693be1ddb --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -0,0 +1,96 @@ +/* + * 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.operation; + +import java.net.URI; +import java.util.Collection; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * A factory for creating {@link OperationRequest OperationRequests}. + * + * @author Andy Wilkinson + */ +public class OperationRequestFactory { + + /** + * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented + * to ensure that they always include a {@code Content-Length} header if the request + * has any content and a {@code Host} header. + * + * @param uri the request's uri + * @param method the request method + * @param content the content of the request + * @param headers the request's headers + * @param parameters the request's parameters + * @param parts the request's parts + * @return the {@code OperationRequest} + */ + public OperationRequest create(URI uri, HttpMethod method, byte[] content, + HttpHeaders headers, Parameters parameters, + Collection parts) { + return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, + uri, content), parameters, parts); + } + + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newContent}. If the original request had a {@code Content-Length} + * header it will be modified to match the length of the new content. + * + * @param original The original request + * @param newContent The new content + * + * @return The new request with the new content + */ + public OperationRequest createFrom(OperationRequest original, byte[] newContent) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + newContent, getUpdatedHeaders(original.getHeaders(), newContent), + original.getParameters(), original.getParts()); + } + + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newHeaders}. + * + * @param original The original request + * @param newHeaders The new headers + * + * @return The new request with the new content + */ + public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + original.getContent(), newHeaders, original.getParameters(), + original.getParts()); + } + + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, + byte[] content) { + return new HttpHeadersHelper(originalHeaders) + .addIfAbsent(HttpHeaders.HOST, uri.getHost()) + .setContentLengthHeader(content).getHeaders(); + } + + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, + byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( + updatedContent).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java new file mode 100644 index 000000000..47aa7e431 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java @@ -0,0 +1,49 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * A factory for creating {@link OperationRequestPart OperationRequestParts}. + * + * @author Andy Wilkinson + */ +public class OperationRequestPartFactory { + + /** + * Creates a new {@link OperationRequestPart}. The given {@code headers} will be + * augmented to ensure that they always include a {@code Content-Length} header if the + * part has any content. + * + * @param name the name of the part + * @param submittedFileName the name of the file being submitted by the part + * @param content the content of the part + * @param headers the headers of the part + * @return the {@code OperationRequestPart} + */ + public OperationRequestPart create(String name, String submittedFileName, + byte[] content, HttpHeaders headers) { + return new StandardOperationRequestPart(name, submittedFileName, content, + augmentHeaders(headers, content)); + } + + private HttpHeaders augmentHeaders(HttpHeaders input, byte[] content) { + return new HttpHeadersHelper(input).setContentLengthHeader(content).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java new file mode 100644 index 000000000..c3417c9f8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -0,0 +1,85 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * A factory for creating {@link OperationResponse OperationResponses}. + * + * @author Andy Wilkinson + */ +public class OperationResponseFactory { + + /** + * Creates a new {@link OperationResponse}. If the response has any content, the given + * {@code headers} will be augmented to ensure that they include a + * {@code Content-Length} header. + * + * @param status the status of the response + * @param headers the request's headers + * @param content the content of the request + * @return the {@code OperationResponse} + */ + public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), + content); + } + + /** + * Creates a new {@code OperationResponse} based on the given {@code original} but + * with the given {@code newContent}. If the original response had a + * {@code Content-Length} header it will be modified to match the length of the new + * content. + * + * @param original The original response + * @param newContent The new content + * + * @return The new response with the new content + */ + public OperationResponse createFrom(OperationResponse original, byte[] newContent) { + return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders( + original.getHeaders(), newContent), newContent); + } + + /** + * Creates a new {@code OperationResponse} based on the given {@code original} but + * with the given {@code newHeaders}. + * + * @param original The original response + * @param newHeaders The new headers + * + * @return The new response with the new headers + */ + public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { + return new StandardOperationResponse(original.getStatus(), newHeaders, + original.getContent()); + } + + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) { + return new HttpHeadersHelper(originalHeaders).setContentLengthHeader(content) + .getHeaders(); + } + + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, + byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( + updatedContent).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index c869100b4..305d7b2a7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -28,7 +28,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationRequest extends AbstractOperationMessage implements +class StandardOperationRequest extends AbstractOperationMessage implements OperationRequest { private HttpMethod method; @@ -50,7 +50,7 @@ public class StandardOperationRequest extends AbstractOperationMessage implement * @param parameters the parameters * @param parts the parts */ - public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, + StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { super(content, headers); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index c0f6e8917..8ea231a4d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -23,7 +23,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationRequestPart extends AbstractOperationMessage implements +class StandardOperationRequestPart extends AbstractOperationMessage implements OperationRequestPart { private final String name; @@ -38,8 +38,8 @@ public class StandardOperationRequestPart extends AbstractOperationMessage imple * @param content the contents of the part * @param headers the headers of the part */ - public StandardOperationRequestPart(String name, String submittedFileName, - byte[] content, HttpHeaders headers) { + StandardOperationRequestPart(String name, String submittedFileName, byte[] content, + HttpHeaders headers) { super(content, headers); this.name = name; this.submittedFileName = submittedFileName; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 1e42beb20..32dfa57f7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -24,7 +24,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationResponse extends AbstractOperationMessage implements +class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { private final HttpStatus status; @@ -37,8 +37,7 @@ public class StandardOperationResponse extends AbstractOperationMessage implemen * @param headers the headers of the response * @param content the content of the response */ - public StandardOperationResponse(HttpStatus status, HttpHeaders headers, - byte[] content) { + StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) { super(content, headers); this.status = status; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 5cc31561b..60c704aa6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -16,11 +16,10 @@ package org.springframework.restdocs.operation.preprocess; -import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content @@ -30,6 +29,10 @@ */ public class ContentModifyingOperationPreprocessor implements OperationPreprocessor { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final ContentModifier contentModifier; /** @@ -46,30 +49,14 @@ public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { public OperationRequest preprocess(OperationRequest request) { byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent(), request.getHeaders().getContentType()); - return new StandardOperationRequest(request.getUri(), request.getMethod(), - modifiedContent, - getUpdatedHeaders(request.getHeaders(), modifiedContent), - request.getParameters(), request.getParts()); + return this.requestFactory.createFrom(request, modifiedContent); } @Override public OperationResponse preprocess(OperationResponse response) { byte[] modifiedContent = this.contentModifier.modifyContent( response.getContent(), response.getHeaders().getContentType()); - return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders( - response.getHeaders(), modifiedContent), modifiedContent); - } - - private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) { - HttpHeaders updatedHeaders = new HttpHeaders(); - updatedHeaders.putAll(headers); - if (updatedContent.length > 0) { - updatedHeaders.setContentLength(updatedContent.length); - } - else { - updatedHeaders.remove(HttpHeaders.CONTENT_LENGTH); - } - return updatedHeaders; + return this.responseFactory.createFrom(response, modifiedContent); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 36eba207c..7766af57c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -22,9 +22,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * An {@link OperationPreprocessor} that removes headers. @@ -33,6 +33,10 @@ */ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final Set headersToRemove; HeaderRemovingOperationPreprocessor(String... headersToRemove) { @@ -41,15 +45,14 @@ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { @Override public OperationResponse preprocess(OperationResponse response) { - return new StandardOperationResponse(response.getStatus(), - removeHeaders(response.getHeaders()), response.getContent()); + return this.responseFactory.createFrom(response, + removeHeaders(response.getHeaders())); } @Override public OperationRequest preprocess(OperationRequest request) { - return new StandardOperationRequest(request.getUri(), request.getMethod(), - request.getContent(), removeHeaders(request.getHeaders()), - request.getParameters(), request.getParts()); + return this.requestFactory.createFrom(request, + removeHeaders(request.getHeaders())); } private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 9ccb89f1d..f948fb103 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -54,8 +54,8 @@ public class HttpRequestSnippetTests { @Test public void getRequest() throws IOException { this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, - "localhost").header("Alpha", "a")); + httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( + HttpHeaders.HOST, "localhost")); new HttpRequestSnippet().document(new OperationBuilder("get-request", this.snippet.getOutputDirectory()).request("http://localhost/foo") @@ -92,8 +92,8 @@ public void postRequestWithCharset() throws IOException { byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpRequest("post-request-with-charset").withContents( httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); @@ -151,10 +151,9 @@ public void multipartPost() throws IOException { + "form-data; " + "name=image%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()).request("http://localhost/upload") .method("POST") @@ -175,10 +174,9 @@ public void multipartPostWithParameters() throws IOException { String expectedContent = param1Part + param2Part + param3Part + filePart; this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( "multipart-post-with-parameters", this.snippet.getOutputDirectory()) .request("http://localhost/upload").method("POST") @@ -194,10 +192,9 @@ public void multipartPostWithContentType() throws IOException { + "image/png%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( "multipart-post-with-content-type", this.snippet.getOutputDirectory()) .request("http://localhost/upload").method("POST") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 5157e07bb..4ff3dbb4e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -26,7 +26,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -38,13 +39,15 @@ */ public class ContentTypeLinkExtractorTests { + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse( + new ContentTypeLinkExtractor().extractLinks(this.responseFactory.create( HttpStatus.OK, new HttpHeaders(), null)); } @@ -55,7 +58,7 @@ public void extractorCalledWithMatchingContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); @@ -68,7 +71,7 @@ public void extractorCalledWithCompatibleContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar")); - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 02adf5cbb..47a05424a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,6 +46,8 @@ @RunWith(Parameterized.class) public class LinkExtractorsPayloadTests { + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final LinkExtractor linkExtractor; private final String linkType; @@ -107,7 +109,7 @@ private void assertLinks(List expectedLinks, Map> actua } private OperationResponse createResponse(String contentName) throws IOException { - return new StandardOperationResponse(HttpStatus.OK, null, + return this.responseFactory.create(HttpStatus.OK, null, FileCopyUtils.copyToByteArray(getPayloadFile(contentName))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 2f38bbb36..330381f68 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -25,11 +25,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -43,6 +43,10 @@ */ public class ContentModifyingOperationPreprocessorTests { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final ContentModifyingOperationPreprocessor preprocessor = new ContentModifyingOperationPreprocessor( new ContentModifier() { @@ -55,7 +59,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { @Test public void modifyRequestContent() { - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), new HttpHeaders(), new Parameters(), Collections.emptyList()); @@ -65,7 +69,7 @@ public void modifyRequestContent() { @Test public void modifyResponseContent() { - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), "content".getBytes()); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); @@ -75,7 +79,7 @@ public void modifyResponseContent() { public void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(7); - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), httpHeaders, new Parameters(), Collections.emptyList()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 808b9ab91..634ef6162 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -25,11 +25,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -44,23 +44,29 @@ */ public class HeaderRemovingOperationPreprocessorTests { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( "b"); @Test public void modifyRequestHeaders() { - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, new byte[0], getHttpHeaders(), new Parameters(), Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); + assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + assertThat(preprocessed.getHeaders(), + hasEntry("Host", Arrays.asList("localhost"))); } @Test public void modifyResponseHeaders() { - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, getHttpHeaders(), new byte[0]); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 51d66504a..e5be81665 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -29,13 +29,13 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationRequestPart; -import org.springframework.restdocs.operation.StandardOperationResponse; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -122,7 +122,7 @@ private OperationRequest buildRequest() { for (OperationRequestPartBuilder builder : this.partBuilders) { parts.add(builder.buildPart()); } - return new StandardOperationRequest(this.requestUri, this.method, + return new OperationRequestFactory().create(this.requestUri, this.method, this.content, this.headers, this.parameters, parts); } @@ -196,7 +196,7 @@ public Operation build() { } private OperationRequestPart buildPart() { - return new StandardOperationRequestPart(this.name, + return new OperationRequestPartFactory().create(this.name, this.submittedFileName, this.content, this.headers); } @@ -219,7 +219,8 @@ public final class OperationResponseBuilder { private byte[] content = new byte[0]; private OperationResponse buildResponse() { - return new StandardOperationResponse(this.status, this.headers, this.content); + return new OperationResponseFactory().create(this.status, this.headers, + this.content); } public OperationResponseBuilder status(int status) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 417f29e82..9449354a8 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -33,10 +33,10 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationRequestPart; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -67,7 +67,7 @@ class MockMvcOperationRequestFactory { * @return the {@code OperationRequest} * @throws Exception if the request could not be created */ - public OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) + OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) throws Exception { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); @@ -76,8 +76,9 @@ public OperationRequest createOperationRequest(MockHttpServletRequest mockReques if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { queryString = parameters.toQueryString(); } - return new StandardOperationRequest(URI.create(getRequestUri(mockRequest) - + (StringUtils.hasText(queryString) ? "?" + queryString : "")), + return new OperationRequestFactory().create( + URI.create(getRequestUri(mockRequest) + + (StringUtils.hasText(queryString) ? "?" + queryString : "")), HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, parameters, parts); @@ -102,16 +103,17 @@ private List extractServletRequestParts( return parts; } - private StandardOperationRequestPart createOperationRequestPart(Part part) - throws IOException { + private OperationRequestPart createOperationRequestPart(Part part) throws IOException { HttpHeaders partHeaders = extractHeaders(part); List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); if (part.getContentType() != null && contentTypeHeader == null) { partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); } - return new StandardOperationRequestPart(part.getName(), StringUtils.hasText(part - .getSubmittedFileName()) ? part.getSubmittedFileName() : null, - FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); + return new OperationRequestPartFactory() + .create(part.getName(), + StringUtils.hasText(part.getSubmittedFileName()) ? part + .getSubmittedFileName() : null, FileCopyUtils + .copyToByteArray(part.getInputStream()), partHeaders); } private List extractMultipartRequestParts( @@ -126,14 +128,14 @@ private List extractMultipartRequestParts( return parts; } - private StandardOperationRequestPart createOperationRequestPart(MultipartFile file) + private OperationRequestPart createOperationRequestPart(MultipartFile file) throws IOException { HttpHeaders partHeaders = new HttpHeaders(); if (StringUtils.hasText(file.getContentType())) { partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); } - return new StandardOperationRequestPart(file.getName(), StringUtils.hasText(file - .getOriginalFilename()) ? file.getOriginalFilename() : null, + return new OperationRequestPartFactory().create(file.getName(), StringUtils + .hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null, file.getBytes(), partHeaders); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java index 549156976..4e4f765f5 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java @@ -20,7 +20,7 @@ import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * A factory for creating an {@link OperationResponse} derived from a @@ -36,8 +36,8 @@ class MockMvcOperationResponseFactory { * @param mockResponse the response * @return the {@code OperationResponse} */ - public OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { - return new StandardOperationResponse( + OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { + return new OperationResponseFactory().create( HttpStatus.valueOf(mockResponse.getStatus()), extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ad6f34539..a7422f943 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -269,33 +269,31 @@ public void preprocessedRequest() throws Exception { .andDo(document("original-request")) .andDo(document( "preprocessed-request", - preprocessRequest(prettyPrint(), removeHeaders("a"), + preprocessRequest( + prettyPrint(), + removeHeaders("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH), replacePattern(pattern, "\"<>\"")))); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("Host", "localhost") - .header("a", "alpha").header("b", "bravo") + httpRequest(RequestMethod.GET, "/").header("a", "alpha") + .header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Host", "localhost") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet() - .withContents( - httpRequest(RequestMethod.GET, "/") - .header("Host", "localhost") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) - .header("Content-Length", - Integer.toString(prettyPrinted.getBytes().length)) - .content(prettyPrinted)))); + is(snippet().withContents( + httpRequest(RequestMethod.GET, "/").header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test From 0d2c24ccc16ff6f2e69356dfa1639965c9633b57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 16:18:27 +0100 Subject: [PATCH 0164/1059] Improve curl request snippet's handling of query parameters Previously, if a request had no content and it was a POST or a PUT request, the curl request snippet would use the request's parameters as application/x-www-form-urlencoded data sent using the -d option. This resulted in the wrong snippet being produced if those parameters had actually come from the request's query string. This commit enhances CurlRequestSnippet so that, when it's using the request's parameter's to create its content, it only includes parameters that are not specified in the query string. With these changes in place, this MockMvc request: post("/?foo=bar").param("foo", "bar").param("a", "alpha") Will produce a curl snippet with the following command: $ curl 'http://localhost:8080/?foo=bar' -i -X POST -d 'a=alpha' foo=bar only appears in the command's query string, despite also being a general request parameter, and a=alpha only appears in the request's data. Closes gh-139 --- .../restdocs/curl/CurlRequestSnippet.java | 39 +++++++- .../restdocs/curl/QueryStringParser.java | 83 +++++++++++++++++ .../curl/CurlRequestSnippetTests.java | 41 ++++++++- .../restdocs/curl/QueryStringParserTests.java | 92 +++++++++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 19 ++++ 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 73829f249..56f1fd4ca 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -31,6 +31,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; @@ -165,9 +166,41 @@ else if (!request.getParts().isEmpty()) { } } else if (isPutOrPost(request)) { - String queryString = request.getParameters().toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } + + private Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java new file mode 100644 index 000000000..d09d6e441 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -0,0 +1,83 @@ +/* + * 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.curl; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Scanner; + +import org.springframework.restdocs.operation.Parameters; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + encoded + + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f84cf0e18..21c1973d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -84,7 +84,7 @@ public void requestWithContent() throws IOException { } @Test - public void requestWithQueryString() throws IOException { + public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") .withContents( codeBlock("bash").content( @@ -94,6 +94,16 @@ public void requestWithQueryString() throws IOException { "http://localhost/foo?param=value").build()); } + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string", this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").method("POST").build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( @@ -132,6 +142,35 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .method("POST").param("k1", "a&b").build()); } + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string-and-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-overlapping-query-string-and-parameters", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); + } + @Test public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java new file mode 100644 index 000000000..bfdc70107 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-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.curl; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link QueryStringParser}. + * + * @author Andy Wilkinson + */ +public class QueryStringParserTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final QueryStringParser queryStringParser = new QueryStringParser(); + + @Test + public void noParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost")); + assertThat(parameters.size(), is(equalTo(0))); + } + + @Test + public void singleParameter() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + } + + @Test + public void multipleParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha&b=bravo&c=charlie")); + assertThat(parameters.size(), is(equalTo(3))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); + assertThat(parameters, hasEntry("c", Arrays.asList("charlie"))); + } + + @Test + public void multipleParametersWithSameKey() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=apple&a=avocado")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); + } + + @Test + public void encoded() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=al%26%3Dpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); + } + + @Test + public void malformedParameter() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage(equalTo("The parameter 'a=apple=avocado' is malformed")); + this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado")); + } +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7422f943..cbf725447 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -136,6 +136,25 @@ public void curlSnippetWithContent() throws Exception { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform( + post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From 5a010160a15d2907647358d76b34b46a62a88c78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 20:11:11 +0100 Subject: [PATCH 0165/1059] Polish Snippet-related javadoc Closes gh-136 --- .../restdocs/curl/CurlDocumentation.java | 16 +-- .../restdocs/http/HttpDocumentation.java | 27 ++--- .../hypermedia/HypermediaDocumentation.java | 99 +++++++++++++------ .../payload/PayloadDocumentation.java | 81 +++++++-------- .../request/RequestDocumentation.java | 73 ++++++++------ .../mockmvc/MockMvcRestDocumentation.java | 24 ++--- .../RestDocumentationMockMvcConfigurer.java | 18 ++++ 7 files changed, 209 insertions(+), 129 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index be692cc93..a3549443b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -36,22 +36,22 @@ private CurlDocumentation() { } /** - * Returns a handler that will produce a snippet containing the curl request for the - * API call. + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. * - * @return the handler that will produce the snippet + * @return the snippet that will document the curl request */ public static Snippet curlRequest() { return new CurlRequestSnippet(); } /** - * Returns a handler that will produce a snippet containing the curl request for the - * API call. The given {@code attributes} will be available during snippet generation. + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * - * @param attributes Attributes made available during rendering of the curl request - * snippet - * @return the handler that will produce the snippet + * @param attributes the attributes + * @return the snippet that will document the curl request */ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 9aae38b71..f1bc8b389 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -33,41 +33,44 @@ private HttpDocumentation() { } /** - * Returns a handler that will produce a snippet containing the HTTP request for the - * API call. + * Returns a new {@code Snippet} that will document the HTTP request for the API + * operation. * - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP request */ public static Snippet httpRequest() { return new HttpRequestSnippet(); } /** - * Returns a handler that will produce a snippet containing the HTTP request for the - * API call. The given {@code attributes} will be available during snippet generation. + * Returns a new {@code Snippet} that will document the HTTP request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * * @param attributes the attributes - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP request */ public static Snippet httpRequest(Map attributes) { return new HttpRequestSnippet(attributes); } /** - * Returns a handler that will produce a snippet containing the HTTP response for the - * API call. - * @return the handler that will produce the snippet + * Returns a {@code Snippet} that will document the HTTP response for the API + * operation. + * + * @return the snippet that will document the HTTP response */ public static Snippet httpResponse() { return new HttpResponseSnippet(); } /** - * Returns a handler that will produce a snippet containing the HTTP response for the - * API call. The given {@code attributes} will be available during snippet generation. + * Returns a {@code Snippet} that will document the HTTP response for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * * @param attributes the attributes - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP response */ public static Snippet httpResponse(Map attributes) { return new HttpResponseSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index b167225e7..c073484e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -43,12 +43,17 @@ public static LinkDescriptor linkWithRel(String rel) { } /** - * Returns a handler that will produce a snippet documenting the links in the API - * call's response. Links will be extracted from the response automatically based on - * its content type. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

    + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param descriptors The descriptions of the response's links - * @return the handler + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), @@ -56,14 +61,19 @@ public static Snippet links(LinkDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the links in the API - * call's response. The given {@code attributes} will be available during snippet - * generation. Links will be extracted from the response automatically based on its - * content type. + * Returns a new {@code Snippet} that will document the links in the API call's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response automatically based on its content type + * and will be documented using the given {@code descriptors}. + *

    + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param attributes Attributes made available during rendering of the links snippet - * @param descriptors The descriptions of the response's links - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(Map attributes, LinkDescriptor... descriptors) { @@ -72,13 +82,18 @@ public static Snippet links(Map attributes, } /** - * Returns a handler that will produce a snippet documenting the links in the API - * call's response. Links will be extracted from the response using the given - * {@code linkExtractor}. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

    + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param linkExtractor Used to extract the links from the response - * @param descriptors The descriptions of the response's links - * @return the handler + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { @@ -86,15 +101,20 @@ public static Snippet links(LinkExtractor linkExtractor, } /** - * Returns a handler that will produce a snippet documenting the links in the API - * call's response. The given {@code attributes} will be available during snippet - * generation. Links will be extracted from the response using the given - * {@code linkExtractor}. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

    + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param attributes Attributes made available during rendering of the links snippet - * @param linkExtractor Used to extract the links from the response - * @param descriptors The descriptions of the response's links - * @return the handler + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { @@ -104,9 +124,19 @@ public static Snippet links(LinkExtractor linkExtractor, /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named - * {@code _links}. + * {@code _links}. For example: * - * @return The extract for HAL-style links + *

    +	 * {
    +	 *     "_links": {
    +	 *         "self": {
    +	 *             "href": "http://example.com/foo"
    +	 *         }
    +	 *     }
    +	 * }
    +	 * 
    + * + * @return The extractor for HAL-style links */ public static LinkExtractor halLinks() { return new HalLinkExtractor(); @@ -114,7 +144,18 @@ public static LinkExtractor halLinks() { /** * Returns a {@code LinkExtractor} capable of extracting links in Atom format where - * the links are found in an array named {@code links}. + * the links are found in an array named {@code links}. For example: + * + *
    +	 * {
    +	 *     "links": [
    +	 *         {
    +	 *             "rel": "self",
    +	 *             "href": "http://example.com/foo"
    +	 *         }
    +	 *     ]
    +     * }
    +	 * 
    * * @return The extractor for Atom-style links */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 653442ed5..56f8266fd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -99,18 +99,18 @@ public static FieldDescriptor fieldWithPath(String path) { } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's request. + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. *

    - * If a field is present in the request, but is not documented by one of the - * descriptors, a failure will occur when the handler is invoked. Similarly, if a + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the request, * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. * - * @param descriptors The descriptions of the request's fields - * @return the handler + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet requestFields(FieldDescriptor... descriptors) { @@ -118,20 +118,20 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's request. The given {@code attributes} will be available during snippet - * generation. + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. *

    - * If a field is present in the request, but is not documented by one of the - * descriptors, a failure will occur when the handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the request's fields - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet requestFields(Map attributes, @@ -140,18 +140,19 @@ public static Snippet requestFields(Map attributes, } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's response. + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . *

    - * If a field is present in the response, but is not documented by one of the - * descriptors, a failure will occur when the handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. * - * @param descriptors The descriptions of the response's fields - * @return the handler + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet responseFields(FieldDescriptor... descriptors) { @@ -159,20 +160,20 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's response. The given {@code attributes} will be available during snippet - * generation. + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. *

    - * If a field is present in the response, but is not documented by one of the - * descriptors, a failure will occur when the handler is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the response's fields - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet responseFields(Map attributes, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 345ec58cc..61c33d5fe 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -19,11 +19,8 @@ import java.util.Arrays; import java.util.Map; -import javax.servlet.ServletRequest; - +import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -48,26 +45,36 @@ public static ParameterDescriptor parameterWithName(String name) { } /** - * Returns a snippet that will document the path parameters from the API call's - * request. + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

    + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. * - * @param descriptors The descriptions of the parameters in the request's path - * @return the snippet - * @see PathVariable + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters */ public static Snippet pathParameters(ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a snippet that will document the path parameters from the API call's - * request. The given {@code attributes} will be available during snippet rendering. + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

    + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. * - * @param attributes Attributes made available during rendering of the path parameters - * snippet - * @param descriptors The descriptions of the parameters in the request's path - * @return the snippet - * @see PathVariable + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters */ public static Snippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { @@ -75,28 +82,38 @@ public static Snippet pathParameters(Map attributes, } /** - * Returns a snippet that will document the request parameters from the API call's - * request. + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

    + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. * * @param descriptors The descriptions of the request's parameters * @return the snippet - * @see RequestParam - * @see ServletRequest#getParameterMap() + * @see OperationRequest#getParameters() */ public static Snippet requestParameters(ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a snippet that will document the request parameters from the API call's - * request. The given {@code attributes} will be available during snippet rendering. + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

    + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. * - * @param attributes Attributes made available during rendering of the request - * parameters snippet - * @param descriptors The descriptions of the request's parameters - * @return the snippet - * @see RequestParam - * @see ServletRequest#getParameterMap() + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() */ public static Snippet requestParameters(Map attributes, ParameterDescriptor... descriptors) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index a41ed1cf1..bedd3753a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -51,10 +51,10 @@ public static RestDocumentationMockMvcConfigurer documentationConfiguration( /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. + * {@code snippets} in addition to any default snippets. * * @param identifier an identifier for the API call that is being documented - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) @@ -66,12 +66,12 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code requestPreprocessor} is applied to the request - * before it is documented. + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) @@ -84,12 +84,12 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code responsePreprocessor} is applied to the request - * before it is documented. + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. * * @param identifier an identifier for the API call that is being documented * @param responsePreprocessor the response preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) @@ -102,14 +102,14 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code requestPreprocessor} and - * {@code responsePreprocessor} are applied to the request and response respectively - * before they are documented. + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param responsePreprocessor the response preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 536f831d4..20f06771c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -19,6 +19,8 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -33,6 +35,22 @@ /** * A {@link MockMvcConfigurer} that can be used to configure the documentation. + *

    + * In the absence of any {@link #snippets() customization} the following snippets will be + * produced by default: + *

      + *
    • {@link CurlDocumentation#curlRequest() Curl request}
    • + *
    • {@link HttpDocumentation#httpRequest() HTTP request}
    • + *
    • {@link HttpDocumentation#httpResponse() HTTP response}
    • + *
    + *

    + * In the absence of any {@link #uris() customization}, documented URIs have the following + * defaults: + *

      + *
    • Scheme: {@code http}
    • + *
    • Host: {@code localhost}
    • + *
    • Port: {@code 8080}
    • + *
    * * @author Andy Wilkinson * @author Dmitriy Mayboroda From 48f4d1343ac2cecd1ad379ed485da5fdf2c39574 Mon Sep 17 00:00:00 2001 From: andreasevers Date: Sun, 20 Sep 2015 12:44:33 +0200 Subject: [PATCH 0166/1059] Add support for documenting HTTP headers This commit adds support for documenting HTTP headers in both requests and responses. Closes gh-140 --- config/checkstyle/checkstyle.xml | 2 +- .../src/main/asciidoc/api-guide.adoc | 7 + .../com/example/notes/ApiDocumentation.java | 11 ++ .../headers/AbstractHeadersSnippet.java | 147 ++++++++++++++++ .../restdocs/headers/HeaderDescriptor.java | 72 ++++++++ .../restdocs/headers/HeaderDocumentation.java | 116 ++++++++++++ .../headers/RequestHeadersSnippet.java | 63 +++++++ .../headers/ResponseHeadersSnippet.java | 63 +++++++ .../restdocs/headers/package-info.java | 20 +++ .../templates/default-request-headers.snippet | 9 + .../default-response-headers.snippet | 9 + .../headers/RequestHeadersSnippetTests.java | 164 +++++++++++++++++ .../headers/ResponseHeadersSnippetTests.java | 165 ++++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 10 ++ .../request-headers-with-extra-column.snippet | 10 ++ .../request-headers-with-title.snippet | 10 ++ ...response-headers-with-extra-column.snippet | 10 ++ .../response-headers-with-title.snippet | 10 ++ 18 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 117567d12..676822798 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 58c579bbc..c0a42afde 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -58,6 +58,13 @@ use of HTTP status codes. | The requested resource did not exist |=== +[[overview-headers]] +== Headers + +Every response has the following header(s): + +include::{snippets}/headers-example/response-headers.adoc[] + [[overview-errors]] == Errors 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 81a7fef7a..4da12b336 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 @@ -18,6 +18,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -98,6 +100,15 @@ public void setUp() { .alwaysDo(this.document) .build(); } + + @Test + public void headersExample() throws Exception { + this.document.snippets(responseHeaders( + headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); + + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()); + } @Test public void errorExample() throws Exception { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java new file mode 100644 index 000000000..2d71e7fcd --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -0,0 +1,147 @@ +/* + * 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.headers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response headers. + * + * @author Andreas Evers + */ +public abstract class AbstractHeadersSnippet extends TemplatedSnippet { + + private List headerDescriptors; + + private String type; + + /** + * Creates a new {@code AbstractHeadersSnippet} that will produce a snippet named + * {@code -headers}. The headers will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * + * @param type the type of the headers + * @param descriptors the header descriptors + * @param attributes the additional attributes + */ + protected AbstractHeadersSnippet(String type, List descriptors, + Map attributes) { + super(type + "-headers", attributes); + for (HeaderDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName()); + Assert.notNull(descriptor.getDescription()); + } + this.headerDescriptors = descriptors; + this.type = type; + } + + @Override + protected Map createModel(Operation operation) { + validateHeaderDocumentation(operation); + + Map model = new HashMap<>(); + List> headers = new ArrayList<>(); + model.put("headers", headers); + for (HeaderDescriptor descriptor : this.headerDescriptors) { + headers.add(createModelForDescriptor(descriptor)); + } + return model; + } + + private void validateHeaderDocumentation(Operation operation) { + List missingHeaders = findMissingHeaders(operation); + + if (!missingHeaders.isEmpty()) { + String message = ""; + if (!missingHeaders.isEmpty()) { + List names = new ArrayList(); + for (HeaderDescriptor headerDescriptor : missingHeaders) { + names.add(headerDescriptor.getName()); + } + message += "Headers with the following names were not found in the " + + this.type + ": " + names; + } + throw new SnippetException(message); + } + } + + /** + * Finds the headers that are missing from the operation. A header is missing if + * it is described by one of the {@code headerDescriptors} but is not present in the + * operation. + * + * @param operation the operation + * @return descriptors for the headers that are missing from the operation + */ + protected List findMissingHeaders(Operation operation) { + List missingHeaders = new ArrayList(); + for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { + if (!headerDescriptor.isOptional() + && !getHeaders(operation).contains(headerDescriptor.getName())) { + missingHeaders.add(headerDescriptor); + } + } + + return missingHeaders; + } + + /** + * Returns the headers of the request or response extracted form the given + * {@code operation}. + * + * @param operation The operation + * @return The headers + */ + protected abstract Set getHeaders(Operation operation); + + /** + * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to + * generate the documentation. + * + * @return the header descriptors + */ + protected final List getHeaderDescriptors() { + return this.headerDescriptors; + } + + /** + * Returns a model for the given {@code descriptor}. + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(HeaderDescriptor descriptor) { + Map model = new HashMap(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java new file mode 100644 index 000000000..8e544bc2a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -0,0 +1,72 @@ +/* + * 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.headers; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.snippet.AbstractDescriptor; + +/** + * A description of a header found in a request or response. + * + * @author Andreas Evers + * @see HeaderDocumentation#headerWithName(String) + */ +public class HeaderDescriptor extends AbstractDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code HeaderDescriptor} describing the header with the given + * {@code name}. + * @param name the name + * @see HttpHeaders + */ + protected HeaderDescriptor(String name) { + this.name = name; + } + + /** + * Marks the header as optional. + * + * @return {@code this} + */ + public final HeaderDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name for the header. + * + * @return the header name + */ + public String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described header is optional, otherwise {@code false}. + * + * @return {@code true} if the described header is optional, otherwise {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java new file mode 100644 index 000000000..a7d5afc6a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -0,0 +1,116 @@ +/* + * 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.headers; + +import java.util.Arrays; +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API's request and response headers. + * + * @author Andreas Evers + */ +public abstract class HeaderDocumentation { + + private HeaderDocumentation() { + + } + + /** + * Creates a {@code HeaderDescriptor} that describes a header with the given + * {@code name}. + * + * @param name The name of the header + * @return a {@code HeaderDescriptor} ready for further configuration + */ + public static HeaderDescriptor headerWithName(String name) { + return new HeaderDescriptor(name); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. + *

    + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. The given {@code attributes} will be available during snippet + * generation. + *

    + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. + *

    + * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. The given {@code attributes} will be available during snippet + * generation. + *

    + * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java new file mode 100644 index 000000000..deb7efca4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a request. + * + * @author Andreas Evers + * @see HeaderDocumentation#requestHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#requestHeaders(Map, HeaderDescriptor...) + */ +public class RequestHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected RequestHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestHeadersSnippet(List descriptors, + Map attributes) { + super("request", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getRequest().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java new file mode 100644 index 000000000..bb0d5d079 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a response. + * + * @author Andreas Evers + * @see HeaderDocumentation#responseHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#responseHeaders(Map, HeaderDescriptor...) + */ +public class ResponseHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected ResponseHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseHeadersSnippet(List descriptors, + Map attributes) { + super("response", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getResponse().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java new file mode 100644 index 000000000..f27ba176c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the headers of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.headers; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet new file mode 100644 index 000000000..5f14875dc --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet new file mode 100644 index 000000000..5f14875dc --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java new file mode 100644 index 000000000..93e831366 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -0,0 +1,164 @@ +/* + * 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link RequestHeadersSnippet}. + * + * @author Andreas Evers + */ +public class RequestHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectRequestHeaders("request-with-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Accept", "two") // + .row("Accept-Encoding", "three") // + .row("Accept-Language", "four") // + .row("Cache-Control", "five") // + .row("Connection", "six")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Accept").description("two"), // + headerWithName("Accept-Encoding").description("three"), // + headerWithName("Accept-Language").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Connection").description("six"))) // + .document(new OperationBuilder("request-with-headers", this.snippet + .getOutputDirectory()) // + .request("http://localhost") // + .header("X-Test", "test") // + .header("Accept", "*/*") // + .header("Accept-Encoding", "gzip, deflate") // + .header("Accept-Language", "en-US,en;q=0.5") // + .header("Cache-Control", "max-age=0") // + .header("Connection", "keep-alive") // + .build()); + } + + @Test + public void undocumentedRequestHeader() throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-request-header", + this.snippet.getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*").build()); + } + + @Test + public void missingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", this.snippet + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").build()); + } + + @Test + public void requestHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Accept-Encoding", "two", "bravo") // + .row("Accept", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-extra-column")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "request-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") + .header("Accept", "*/*").build()); + } + + @Test + public void requestHeadersWithCustomAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-title")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").header("X-Test", "test").build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java new file mode 100644 index 000000000..ac6a40cee --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -0,0 +1,165 @@ +/* + * 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link ReponseHeadersSnippet}. + * + * @author Andreas Evers + */ +public class ResponseHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void responseWithHeaders() throws IOException { + this.snippet.expectResponseHeaders("response-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Content-Type", "two") // + .row("Etag", "three") // + .row("Content-Length", "four") // + .row("Cache-Control", "five") // + .row("Vary", "six")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Content-Type").description("two"), // + headerWithName("Etag").description("three"), // + headerWithName("Content-Length").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Vary").description("six"))) // + .document(new OperationBuilder("response-headers", this.snippet + .getOutputDirectory()) // + .response() // + .header("X-Test", "test") // + .header("Content-Type", "application/json") // + .header("Etag", "lskjadldj3ii32l2ij23") // + .header("Content-Length", "19166") // + .header("Cache-Control", "max-age=0") // + .header("Vary", "User-Agent") // + .build()); + } + + @Test + public void responseHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Content-Type", "two", "bravo") // + .row("Etag", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-extra-column")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Content-Type").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Etag").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "response-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23").build()); + } + + @Test + public void responseHeadersWithCustomAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").build()); + } + + @Test + public void undocumentedResponseHeader() throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); + } + + @Test + public void missingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "missing-response-headers", this.snippet.getOutputDirectory()).response() + .build()); + } + + @Test + public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", this.snippet + .getOutputDirectory()).response().header("X-Test", "test") + .build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index c20b0df06..475cb15c1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -75,6 +75,16 @@ public ExpectedSnippet expectResponseFields(String name) { return this; } + public ExpectedSnippet expectRequestHeaders(String name) { + expect(name, "request-headers"); + return this; + } + + public ExpectedSnippet expectResponseHeaders(String name) { + expect(name, "response-headers"); + return this; + } + public ExpectedSnippet expectLinks(String name) { expect(name, "links"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet new file mode 100644 index 000000000..29d687777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet new file mode 100644 index 000000000..998090a0d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet new file mode 100644 index 000000000..29d687777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet new file mode 100644 index 000000000..998090a0d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file From ea8f736259ea9095ce41a84b15f848b2133040b0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 29 Sep 2015 12:31:04 +0100 Subject: [PATCH 0167/1059] Polish contribution that added support for documenting HTTP headers The main changes are: - Update javadoc to align with changes made in 5a01016 - Update documentation with details of the new support for documenting HTTP headers - Add tests to verify that HTTP headers are matched case-insensitively Closes gh-71 --- config/checkstyle/checkstyle.xml | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 25 ++++++++ .../test/java/com/example/HttpHeaders.java | 50 ++++++++++++++++ .../headers/AbstractHeadersSnippet.java | 15 ++--- .../restdocs/headers/HeaderDescriptor.java | 2 +- .../restdocs/headers/HeaderDocumentation.java | 54 +++++++++-------- .../headers/RequestHeadersSnippet.java | 2 +- .../headers/ResponseHeadersSnippet.java | 2 +- .../headers/RequestHeadersSnippetTests.java | 56 ++++++++++-------- .../headers/ResponseHeadersSnippetTests.java | 58 ++++++++++--------- .../restdocs/test/ExpectedSnippet.java | 1 + 11 files changed, 175 insertions(+), 92 deletions(-) create mode 100644 docs/src/test/java/com/example/HttpHeaders.java diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 676822798..b7908d746 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7fd84cdf3..af979cb05 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -288,6 +288,31 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than +[[documenting-your-api-http-headers]] +=== HTTP headers + +The headers in a request or response can be documented using `requestHeaders` and +`responseHeaders` respectively. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] +---- +<1> Perform a `GET` request with an `Authorization` header that uses basic authentication +<2> Produce a snippet describing the request's headers. Uses the static `requestHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<3> Document the `Authorization` header. Uses the static `headerWithName` method on +`org.springframework.restdocs.headers.HeaderDocumentation. +<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. + +The result is a snippet named `request-headers.adoc` and a snippet named +`response-headers.adoc`. Each contains a table describing the headers. + +When documenting HTTP Headers, the test will fail if a documented header is not found in +the request or response. + + [[documenting-your-api-constraints]] === Documenting constraints diff --git a/docs/src/test/java/com/example/HttpHeaders.java b/docs/src/test/java/com/example/HttpHeaders.java new file mode 100644 index 000000000..f47e39714 --- /dev/null +++ b/docs/src/test/java/com/example/HttpHeaders.java @@ -0,0 +1,50 @@ +/* + * 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 com.example; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HttpHeaders { + + private MockMvc mockMvc; + + public void headers() throws Exception { + // tag::headers[] + this.mockMvc + .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + .andExpect(status().isOk()) + .andDo(document("headers", + requestHeaders( // <2> + headerWithName("Authorization").description( + "Basic auth credentials")), // <3> + responseHeaders( // <4> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))); + // end::headers[] + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index 2d71e7fcd..5c3099b9d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -91,8 +91,8 @@ private void validateHeaderDocumentation(Operation operation) { } /** - * Finds the headers that are missing from the operation. A header is missing if - * it is described by one of the {@code headerDescriptors} but is not present in the + * Finds the headers that are missing from the operation. A header is missing if it is + * described by one of the {@code headerDescriptors} but is not present in the * operation. * * @param operation the operation @@ -100,9 +100,10 @@ private void validateHeaderDocumentation(Operation operation) { */ protected List findMissingHeaders(Operation operation) { List missingHeaders = new ArrayList(); + Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { if (!headerDescriptor.isOptional() - && !getHeaders(operation).contains(headerDescriptor.getName())) { + && !actualHeaders.contains(headerDescriptor.getName())) { missingHeaders.add(headerDescriptor); } } @@ -111,13 +112,13 @@ protected List findMissingHeaders(Operation operation) { } /** - * Returns the headers of the request or response extracted form the given + * Extracts the names of the headers from the request or response of the given * {@code operation}. * - * @param operation The operation - * @return The headers + * @param operation the operation + * @return the header names */ - protected abstract Set getHeaders(Operation operation); + protected abstract Set extractActualHeaders(Operation operation); /** * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java index 8e544bc2a..b0e332abf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -56,7 +56,7 @@ public final HeaderDescriptor optional() { * * @return the header name */ - public String getName() { + public final String getName() { return this.name; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index a7d5afc6a..85800ecff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -25,6 +25,7 @@ * Static factory methods for documenting a RESTful API's request and response headers. * * @author Andreas Evers + * @author Andy Wilkinson */ public abstract class HeaderDocumentation { @@ -44,15 +45,14 @@ public static HeaderDescriptor headerWithName(String name) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's request. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * request. The headers will be documented using the given {@code descriptors}. *

    * If a header is documented, is not marked as optional, and is not present in the - * request, a failure will occur. If a header is present in the request, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { @@ -60,17 +60,16 @@ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's request. The given {@code attributes} will be available during snippet - * generation. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the headers will be documented using the given {@code descriptors}. *

    * If a header is documented, is not marked as optional, and is not present in the - * request, a failure will occur. If a header is present in the request, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(Map attributes, @@ -79,15 +78,14 @@ public static Snippet requestHeaders(Map attributes, } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's response. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * response. The headers will be documented using the given {@code descriptors}. *

    * If a header is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a header is present in the response, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { @@ -95,17 +93,17 @@ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's response. The given {@code attributes} will be available during snippet - * generation. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the headers will be documented using the given + * {@code descriptors}. *

    * If a header is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a header is present in the response, but is not - * documented by one of the descriptors, there will be no failure. + * response, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(Map attributes, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index deb7efca4..83dd6b6d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -56,7 +56,7 @@ protected RequestHeadersSnippet(List descriptors, } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(Operation operation) { return operation.getRequest().getHeaders().keySet(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index bb0d5d079..979083c42 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -56,7 +56,7 @@ protected ResponseHeadersSnippet(List descriptors, } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(Operation operation) { return operation.getResponse().getHeaders().keySet(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 93e831366..b5b2d1e4c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -44,6 +44,7 @@ * Tests for {@link RequestHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class RequestHeadersSnippetTests { @@ -55,31 +56,36 @@ public class RequestHeadersSnippetTests { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers").withContents(// - tableWithHeader("Name", "Description") // - .row("X-Test", "one") // - .row("Accept", "two") // - .row("Accept-Encoding", "three") // - .row("Accept-Language", "four") // - .row("Cache-Control", "five") // + this.snippet.expectRequestHeaders("request-with-headers").withContents( + tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Accept", "two").row("Accept-Encoding", "three") + .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), // - headerWithName("Accept").description("two"), // - headerWithName("Accept-Encoding").description("three"), // - headerWithName("Accept-Language").description("four"), // - headerWithName("Cache-Control").description("five"), // - headerWithName("Connection").description("six"))) // + headerWithName("X-Test").description("one"), headerWithName("Accept") + .description("two"), headerWithName("Accept-Encoding") + .description("three"), headerWithName("Accept-Language") + .description("four"), headerWithName("Cache-Control") + .description("five"), + headerWithName("Connection").description("six"))) .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()) // - .request("http://localhost") // - .header("X-Test", "test") // - .header("Accept", "*/*") // - .header("Accept-Encoding", "gzip, deflate") // - .header("Accept-Language", "en-US,en;q=0.5") // - .header("Cache-Control", "max-age=0") // - .header("Connection", "keep-alive") // - .build()); + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); + } + + @Test + public void caseInsensitiveRequestHeaders() throws IOException { + this.snippet + .expectRequestHeaders("case-insensitive-request-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-request-headers", this.snippet.getOutputDirectory()) + .request("/").header("X-test", "test").build()); } @Test @@ -118,9 +124,9 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio public void requestHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") .withContents(// - tableWithHeader("Name", "Description", "Foo") // - .row("X-Test", "one", "alpha") // - .row("Accept-Encoding", "two", "bravo") // + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")).willReturn( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index ac6a40cee..24487094f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -41,9 +41,10 @@ import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** - * Tests for {@link ReponseHeadersSnippet}. + * Tests for {@link ResponseHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class ResponseHeadersSnippetTests { @@ -55,40 +56,41 @@ public class ResponseHeadersSnippetTests { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents(// - tableWithHeader("Name", "Description") // - .row("X-Test", "one") // - .row("Content-Type", "two") // - .row("Etag", "three") // - .row("Content-Length", "four") // - .row("Cache-Control", "five") // - .row("Vary", "six")); + this.snippet.expectResponseHeaders("response-headers").withContents( + tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Content-Type", "two").row("Etag", "three") + .row("Cache-Control", "five").row("Vary", "six")); new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), // - headerWithName("Content-Type").description("two"), // - headerWithName("Etag").description("three"), // - headerWithName("Content-Length").description("four"), // - headerWithName("Cache-Control").description("five"), // - headerWithName("Vary").description("six"))) // + headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), headerWithName("Etag") + .description("three"), headerWithName("Cache-Control") + .description("five"), headerWithName("Vary").description("six"))) .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()) // - .response() // - .header("X-Test", "test") // - .header("Content-Type", "application/json") // - .header("Etag", "lskjadldj3ii32l2ij23") // - .header("Content-Length", "19166") // - .header("Cache-Control", "max-age=0") // - .header("Vary", "User-Agent") // - .build()); + .getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); + } + + @Test + public void caseInsensitiveResponseHeaders() throws IOException { + this.snippet + .expectResponseHeaders("case-insensitive-response-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-response-headers", this.snippet.getOutputDirectory()) + .response().header("X-test", "test").build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") // - .row("X-Test", "one", "alpha") // - .row("Content-Type", "two", "bravo") // + .withContents( + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")).willReturn( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 475cb15c1..63364f6a6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -34,6 +34,7 @@ * generated the expected snippet. * * @author Andy Wilkinson + * @author Andreas Evers */ public class ExpectedSnippet implements TestRule { From fb01a26a16fd267f7c5de546b69358666d7fe728 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 30 Sep 2015 17:33:29 +0100 Subject: [PATCH 0168/1059] Allow an item to be undocumented without it causing a test failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, it was necessary to document every payload field, link, path parameter, or request. If an item was not documented a failure would occur. This has proven to be too restrictive for some use cases, for example splitting up the documentation of a payload’s fields. While it was possible to use a preprocessor to modify the operation prior to documentation to remove the items that should not be documented, this was more difficult than it needed to be. This commit adds support for marking a descriptor as ignored. Ignored descriptors count when checking that everything has been documented but do not actually appear in the generated documentation. Closes gh-143 --- .../hypermedia/HypermediaDocumentation.java | 16 ++++++ .../restdocs/hypermedia/LinkDescriptor.java | 4 +- .../restdocs/hypermedia/LinksSnippet.java | 13 +++-- .../payload/AbstractFieldsSnippet.java | 13 +++-- .../restdocs/payload/FieldDescriptor.java | 4 +- .../payload/PayloadDocumentation.java | 16 ++++++ .../request/AbstractParametersSnippet.java | 14 +++-- .../restdocs/request/ParameterDescriptor.java | 4 +- .../request/RequestDocumentation.java | 16 ++++++ .../restdocs/snippet/IgnorableDescriptor.java | 52 +++++++++++++++++++ .../hypermedia/LinksSnippetTests.java | 11 ++++ .../payload/RequestFieldsSnippetTests.java | 13 +++++ .../payload/ResponseFieldsSnippetTests.java | 13 +++++ .../request/PathParametersSnippetTests.java | 12 +++++ .../RequestParametersSnippetTests.java | 11 ++++ ...kMvcRestDocumentationIntegrationTests.java | 29 +++++++++++ 16 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index c073484e3..a4fbf42d4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -51,6 +51,10 @@ public static LinkDescriptor linkWithRel(String rel) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

    + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links @@ -70,6 +74,10 @@ public static Snippet links(LinkDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

    + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the response's links @@ -90,6 +98,10 @@ public static Snippet links(Map attributes, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

    + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links @@ -110,6 +122,10 @@ public static Snippet links(LinkExtractor linkExtractor, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

    + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param linkExtractor used to extract the links from the response diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 810a52448..8876cf7de 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.hypermedia; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A description of a link found in a hypermedia API. @@ -24,7 +24,7 @@ * @author Andy Wilkinson * @see HypermediaDocumentation#linkWithRel(String) */ -public class LinkDescriptor extends AbstractDescriptor { +public class LinkDescriptor extends IgnorableDescriptor { private final String rel; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 8b5a04d46..79ae0ee26 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -74,8 +74,12 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getRel()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The descriptor for link '" + + descriptor.getRel() + "' must either have a description or be" + + " marked as " + "ignored"); + } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } } @@ -131,7 +135,10 @@ private void validate(Map> links) { private List> createLinksModel() { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { - model.add(createModelForDescriptor(entry.getValue())); + LinkDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + model.add(createModelForDescriptor(descriptor)); + } } return model; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index c96b58f49..3bf98a83b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -55,8 +55,13 @@ protected AbstractFieldsSnippet(String type, List descriptors, Map attributes) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getPath()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The descriptor for field '" + + descriptor.getPath() + "' must either have a description or" + + " be marked as " + "ignored"); + } + } this.fieldDescriptors = descriptors; } @@ -77,7 +82,9 @@ protected Map createModel(Operation operation) { List> fields = new ArrayList<>(); model.put("fields", fields); for (FieldDescriptor descriptor : this.fieldDescriptors) { - fields.add(createModelForDescriptor(descriptor)); + if (!descriptor.isIgnored()) { + fields.add(createModelForDescriptor(descriptor)); + } } return model; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 7df08f1fd..d68bb68d7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.payload; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A description of a field found in a request or response payload. @@ -25,7 +25,7 @@ * @author Andy Wilkinson * @see PayloadDocumentation#fieldWithPath(String) */ -public class FieldDescriptor extends AbstractDescriptor { +public class FieldDescriptor extends IgnorableDescriptor { private final String path; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 56f8266fd..097e1c47c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -108,6 +108,10 @@ public static FieldDescriptor fieldWithPath(String path) { * a failure will also occur. For payloads with a hierarchical structure, documenting * a field is sufficient for all of its descendants to also be treated as having been * documented. + *

    + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields @@ -128,6 +132,10 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { * payload, a failure will also occur. For payloads with a hierarchical structure, * documenting a field is sufficient for all of its descendants to also be treated as * having been documented. + *

    + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields @@ -150,6 +158,10 @@ public static Snippet requestFields(Map attributes, * payload, a failure will also occur. For payloads with a hierarchical structure, * documenting a field is sufficient for all of its descendants to also be treated as * having been documented. + *

    + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields @@ -170,6 +182,10 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { * payload, a failure will also occur. For payloads with a hierarchical structure, * documenting a field is sufficient for all of its descendants to also be treated as * having been documented. + *

    + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 144c8d672..0763505b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -54,8 +54,13 @@ protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getName()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), + "The descriptor for parameter '" + descriptor.getName() + + "' must either have a description or be marked as " + + "ignored"); + } this.descriptorsByName.put(descriptor.getName(), descriptor); } } @@ -67,7 +72,10 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); for (Entry entry : this.descriptorsByName.entrySet()) { - parameters.add(createModelForDescriptor(entry.getValue())); + ParameterDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + parameters.add(createModelForDescriptor(descriptor)); + } } model.put("parameters", parameters); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index ff9d561dd..9172a5525 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.request; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A descriptor of a request or path parameter. @@ -25,7 +25,7 @@ * @see RequestDocumentation#parameterWithName * */ -public class ParameterDescriptor extends AbstractDescriptor { +public class ParameterDescriptor extends IgnorableDescriptor { private final String name; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 61c33d5fe..e0aa2f4ba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -53,6 +53,10 @@ public static ParameterDescriptor parameterWithName(String name) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request path, a failure will also occur. + *

    + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters @@ -71,6 +75,10 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request path, a failure will also occur. + *

    + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path @@ -90,6 +98,10 @@ public static Snippet pathParameters(Map attributes, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. + *

    + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param descriptors The descriptions of the request's parameters * @return the snippet @@ -109,6 +121,10 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. + *

    + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the request's parameters diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java new file mode 100644 index 000000000..8174c3815 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java @@ -0,0 +1,52 @@ +/* + * 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.snippet; + +/** + * Base class for descriptors for items that can be ignored. + * + * @param the type of the descriptor + * @author Andy Wilkinson + */ +public abstract class IgnorableDescriptor> extends + AbstractDescriptor { + + private boolean ignored = false; + + /** + * Marks the described item as being ignored. Ignored items are not included in the + * generated documentation. + * + * @return the descriptor + */ + @SuppressWarnings("unchecked") + public final T ignored() { + this.ignored = true; + return (T) this; + } + + /** + * Returns whether or not the item being described should be ignored and, therefore, + * should not be included in the documentation. + * + * @return {@code true} if the item should be ignored, otherwise {@code false}. + */ + public final boolean isIgnored() { + return this.ignored; + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 57b9f7556..cb9ac2883 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -65,6 +65,17 @@ public void undocumentedLink() throws IOException { "undocumented-link", this.snippet.getOutputDirectory()).build()); } + @Test + public void ignoredLink() throws IOException { + this.snippet.expectLinks("ignored-link").withContents( + tableWithHeader("Relation", "Description").row("b", "Link b")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), + new LinkDescriptor("b").description("Link b"))) + .document(new OperationBuilder("ignored-link", this.snippet + .getOutputDirectory()).build()); + } + @Test public void missingLink() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 1c1481c80..0fedbdee4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -98,6 +98,19 @@ public void undocumentedRequestField() throws IOException { .content("{\"a\": 5}").build()); } + @Test + public void ignoredRequestField() throws IOException { + this.snippet.expectRequestFields("ignored-request-field").withContents( + tableWithHeader("Path", "Type", "Description").row("b", "Number", + "Field b")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), + fieldWithPath("b").description("Field b"))) + .document(new OperationBuilder("ignored-request-field", this.snippet + .getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void missingRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 56bf56886..337e6e8c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -106,6 +106,19 @@ public void arrayResponse() throws IOException { .content("[\"a\", \"b\", \"c\"]").build()); } + @Test + public void ignoredResponseField() throws IOException { + this.snippet.expectResponseFields("ignored-response-field").withContents( + tableWithHeader("Path", "Type", "Description").row("b", "Number", + "Field b")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), + fieldWithPath("b").description("Field b"))) + .document(new OperationBuilder("ignored-response-field", this.snippet + .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") + .build()); + } + @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index f7f0f462c..4f2ee0b23 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -104,6 +104,18 @@ public void pathParameters() throws IOException { "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); } + @Test + public void ignoredPathParameter() throws IOException { + this.snippet.expectPathParameters("ignored-path-parameter").withContents( + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", + "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("ignored-path-parameter", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + } + @Test public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 9ac769721..da61deecc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -102,6 +102,17 @@ public void requestParameters() throws IOException { .build()); } + @Test + public void ignoredRequestParameter() throws IOException { + this.snippet.expectRequestParameters("ignored-request-parameter").withContents( + tableWithHeader("Parameter", "Description").row("b", "two")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("ignored-request-parameter", this.snippet + .getOutputDirectory()).request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); + } + @Test public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index cbf725447..8c5be89e5 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -400,6 +400,30 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } + @Test + public void stackOverflowQuestion() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document( + "company", + responseFields( + fieldWithPath("companyName").description( + "The name of the company"), + fieldWithPath("employee").description( + "An array of the company's employees")))) + .andDo(document( + "employee", + responseFields( + fieldWithPath("companyName").ignored(), + fieldWithPath("employee[].name").description( + "The name of the employee"), + fieldWithPath("employee[].age").description( + "The age of the employee")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); @@ -434,6 +458,11 @@ public ResponseEntity> foo() { HttpStatus.OK); } + @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) + public String bar() { + return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; + } + } } From 2730a7ceef8e2a34f754cae4fea2e957cb69484a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 30 Sep 2015 19:28:43 +0100 Subject: [PATCH 0169/1059] Polishing --- .../headers/AbstractHeadersSnippet.java | 15 ++++-------- ...kMvcRestDocumentationIntegrationTests.java | 24 ------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index 5c3099b9d..f5e2c1269 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -75,18 +75,13 @@ protected Map createModel(Operation operation) { private void validateHeaderDocumentation(Operation operation) { List missingHeaders = findMissingHeaders(operation); - if (!missingHeaders.isEmpty()) { - String message = ""; - if (!missingHeaders.isEmpty()) { - List names = new ArrayList(); - for (HeaderDescriptor headerDescriptor : missingHeaders) { - names.add(headerDescriptor.getName()); - } - message += "Headers with the following names were not found in the " - + this.type + ": " + names; + List names = new ArrayList(); + for (HeaderDescriptor headerDescriptor : missingHeaders) { + names.add(headerDescriptor.getName()); } - throw new SnippetException(message); + throw new SnippetException("Headers with the following names were not found" + + " in the " + this.type + ": " + names); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 8c5be89e5..546a17e75 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -400,30 +400,6 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } - @Test - public void stackOverflowQuestion() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - - mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document( - "company", - responseFields( - fieldWithPath("companyName").description( - "The name of the company"), - fieldWithPath("employee").description( - "An array of the company's employees")))) - .andDo(document( - "employee", - responseFields( - fieldWithPath("companyName").ignored(), - fieldWithPath("employee[].name").description( - "The name of the employee"), - fieldWithPath("employee[].age").description( - "The age of the employee")))); - } - private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); From 723b7a284f1c4cecffe484f546e3b33a3c44a29b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 5 Oct 2015 10:22:55 +0100 Subject: [PATCH 0170/1059] Use default location for .adoc source in Gradle-based sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously both samples supported Maven and Gradle. Due to the different default locations between the two build systems, the Gradle-based sample was configured to use Maven’s default location for its .adoc files. The samples have now been updated so that one sample uses Maven and the other uses Gradle. However, when this change was made, the customization of the Gradle-based sample’s .adoc file location wasn’t removed. This commit updates the Gradle-based sample to use the default location, src/docs/asciidoc, for its .adoc source files. Closes gh-145 --- samples/rest-notes-spring-hateoas/build.gradle | 1 - .../src/{main => docs}/asciidoc/api-guide.adoc | 0 .../src/{main => docs}/asciidoc/getting-started-guide.adoc | 0 3 files changed, 1 deletion(-) rename samples/rest-notes-spring-hateoas/src/{main => docs}/asciidoc/api-guide.adoc (100%) rename samples/rest-notes-spring-hateoas/src/{main => docs}/asciidoc/getting-started-guide.adoc (100%) diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a099b066e..f6c8ca4a3 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -48,7 +48,6 @@ test { } asciidoctor { - sourceDir 'src/main/asciidoc' // Align with Maven's default location attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc similarity index 100% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc rename to samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc similarity index 100% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc rename to samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc From a693d12ce0b3efa2d4ef666753c0c44537b29ae5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:29:23 +0100 Subject: [PATCH 0171/1059] Polish the documentation --- docs/src/docs/asciidoc/contributing.adoc | 6 +- .../docs/asciidoc/documenting-your-api.adoc | 74 ++++++++++++------- docs/src/docs/asciidoc/getting-started.adoc | 15 ++-- docs/src/docs/asciidoc/index.adoc | 6 +- docs/src/docs/asciidoc/introduction.adoc | 4 +- 5 files changed, 62 insertions(+), 43 deletions(-) diff --git a/docs/src/docs/asciidoc/contributing.adoc b/docs/src/docs/asciidoc/contributing.adoc index 660c3c09e..ce726ed63 100644 --- a/docs/src/docs/asciidoc/contributing.adoc +++ b/docs/src/docs/asciidoc/contributing.adoc @@ -19,7 +19,7 @@ Spring REST Docs users by answering questions. === Bugs If you believe you have found a bug, please take a moment to search the -{github}/issues?is%3Aissue[existing issues]. If no one else has reported the problem, +{github}/issues?q=is%3Aissue[existing issues]. If no one else has reported the problem, please {github}/issues/new[open a new issue] that describes the problem in detail and, ideally, includes a test that reproduces it. @@ -30,7 +30,7 @@ ideally, includes a test that reproduces it. If you'd like an enhancement to be made to Spring REST Docs, pull requests are most welcome. The source code is on {github}[GitHub]. You may want to search the -{github}/issues?is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] to -see if the enhancement is already being worked on. You may also want to +{github}/issues?q=is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] +to see if the enhancement is already being worked on. You may also want to {github}/issues/new[open a new issue] to discuss a possible enhancement before work on it begins. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index af979cb05..c7a184cb5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -15,10 +15,11 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: ---- include::{examples-dir}/com/example/Hypermedia.java[tag=links] ---- -<1> Produce a snippet describing the response's links. Uses the static `links` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<1> Configure Spring REST docs to produce a snippet describing the response's links. + Uses the static `links` method on + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <3> Expect a link whose rel is `bravo`. The result is a snippet named `links.adoc` that contains a table describing the resource's @@ -28,6 +29,9 @@ When documenting links, the test will fail if an undocumented link is found in t response. Similarly, the test will also fail if a documented link is not found in the response and the link has not been marked as optional. +If you do not want to document a link, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + [[documenting-your-api-hypermedia-link-formats]] @@ -65,11 +69,11 @@ provided. For example: ---- include::{examples-dir}/com/example/Payload.java[tags=response] ---- -<1> Use `responseFields` to describe the expected fields in the response payload. -To document a request `requestFields` can be used. Both are static methods on -`org.springframework.restdocs.payload.PayloadDocumentation`. +<1> Configure Spring REST docs to produce a snippet describing the fields in the response + payload. To document a request `requestFields` can be used. Both are static methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. <2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on -`org.springframework.restdocs.payload.PayloadDocumentation`. + `org.springframework.restdocs.payload.PayloadDocumentation`. <3> Expect a field with the path `contact.email`. The result is a snippet that contains a table describing the fields. For requests this @@ -82,6 +86,9 @@ payload and the field has not been marked as optional. For payloads with a hiera structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. +If you do not want to document a field, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + TIP: By default, Spring REST Docs will assume that the payload you are documenting is JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. @@ -195,7 +202,8 @@ examining the payload. Seven different types are supported: |=== The type can also be set explicitly using the `type(Object)` method on -`FieldDescriptor`. Typically, one of the values enumerated by `JsonFieldType` will be +`FieldDescriptor`. The result of the supplied ``Object``'s `toString` method will be used +in the documentation. Typically, one of the values enumerated by `JsonFieldType` will be used: [source,java,indent=0] @@ -220,7 +228,7 @@ XML field paths are described using XPath. `/` is used to descend into a child n When documenting an XML payload, you must provide a type for the field using the `type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` -method will be included in the documentation. +method will be used in the documentation. @@ -236,10 +244,11 @@ include::{examples-dir}/com/example/RequestParameters.java[tags=request-paramete ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. -<2> Produce a snippet describing the request's parameters. Uses the static -`requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. + Uses the static `requestParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on -`org.springframework.restdocs.request.RequestDocumentation`. + `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. Request parameters can also be included as form data in the body of a POST request: @@ -257,6 +266,10 @@ When documenting request parameters, the test will fail if an undocumented reque parameter is used in the request. Similarly, the test will also fail if a documented request parameter is not found in the request. +If you do not want to document a request parameter, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. + [[documenting-your-api-path-parameters]] @@ -269,22 +282,27 @@ A request's path parameters can be documented using `pathParameters`. For exampl include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] ---- <1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. -<2> Produce a snippet describing the request's path parameters. Uses the static -`pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's path + parameters. Uses the static `pathParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the parameter named `latitude`. Uses the static `parameterWithName` method on -`org.springframework.restdocs.request.RequestDocumentation`. + `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. +TIP: To make the path parameters available for documentation, the request must be +built using one of the methods on `RestDocumentationRequestBuilders` rather than +`MockMvcRequestBuilders`. + When documenting path parameters, the test will fail if an undocumented path parameter is used in the request. Similarly, the test will also fail if a documented path parameter is not found in the request. -TIP: To make the path parameters available for documentation, the request must be -built using one of the methods on `RestDocumentationRequestBuilders` rather than -`MockMvcRequestBuilders`. +If you do not want to document a path parameter, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. @@ -299,12 +317,13 @@ The headers in a request or response can be documented using `requestHeaders` an include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] ---- <1> Perform a `GET` request with an `Authorization` header that uses basic authentication -<2> Produce a snippet describing the request's headers. Uses the static `requestHeaders` -method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's headers. + Uses the static `requestHeaders` method on + `org.springframework.restdocs.headers.HeaderDocumentation`. <3> Document the `Authorization` header. Uses the static `headerWithName` method on -`org.springframework.restdocs.headers.HeaderDocumentation. + `org.springframework.restdocs.headers.HeaderDocumentation. <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` -method on `org.springframework.restdocs.headers.HeaderDocumentation`. + method on `org.springframework.restdocs.headers.HeaderDocumentation`. The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -328,8 +347,8 @@ include::{examples-dir}/com/example/Constraints.java[tags=constraints] <2> Get the descriptions of the name property's constraints. This list will contain two descriptions; one for the `NotNull` constraint and one for the `Size` constraint. -The `ApiDocumentation` class in the Spring HATEOAS sample shows this functionality in -action. +The {samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] +class in the Spring HATEOAS sample shows this functionality in action. @@ -457,7 +476,7 @@ are supported: |=== For example, `document("{class-name}/{method-name}")` in a test method named -`creatingANote` on the test class `GettingStartedDocumentaiton`, will write +`creatingANote` on the test class `GettingStartedDocumentation`, will write snippets into a directory named `getting-started-documentation/creating-a-note`. A parameterized output directory is particularly useful in combination with Spring MVC @@ -485,7 +504,7 @@ sample applications to see this functionality in action. ==== Customizing the generated snippets Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the generated snippets. -{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +{source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. @@ -501,8 +520,7 @@ override the template for the `curl-request.adoc` snippet, create a template nam There are two ways to provide extra information for inclusion in a generated snippet: -. Use the `attributes` method on a field, link or parameter descriptor to add one or more - attributes to an individual descriptor. +. Use the `attributes` method on a descriptor to add one or more attributes to it. . Pass in some attributes when calling `curlRequest`, `httpRequest`, `httpResponse`, etc. Such attributes will be associated with the snippet as a whole. diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index ed25c8abf..136d98069 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -15,9 +15,9 @@ http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use Spring REST Docs to produce a detailed API guide and a getting started walkthrough. -In each sample the source for the documentation can be found in `src/main/asciidoc`. -`api-guide.adoc` produces an API guide for the service. `getting-started-guide.adoc` -produces a getting started guide that provides an introductory walkthrough. +Each sample contains a file named `api-guide.adoc` that produces an API guide for the +service, and a file named `getting-started-guide.adoc` that produces a getting started +guide that provides an introductory walkthrough. The code that produces the generated snippets can be found in `src/test/java`. `ApiDocumentation.java` produces the snippets for the API guide. @@ -35,7 +35,7 @@ The first step in using Spring REST Docs is to configure your project's build. [[getting-started-build-configuration-gradle]] ==== Gradle build configuration -The {samples}rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` +The {samples}/rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -99,7 +99,7 @@ directory: [[getting-started-build-configuration-maven]] ==== Maven build configuration -The {samples}rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` +The {samples}/rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -210,7 +210,8 @@ be included in the project's jar: ---- <1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound -to the same phase and the resource plugin must run after the Asciidoctor plugin. +to the same phase (`prepare-package`) and the resource plugin must run after the +Asciidoctor plugin. @@ -218,7 +219,7 @@ to the same phase and the resource plugin must run after the Asciidoctor plugin. === Generating documentation snippets Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] to make requests to the service that you are documenting. It then produces -documentation snippets for the result's request and response. +documentation snippets for request and resulting response. diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index d4821b024..86c27c47f 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -9,9 +9,9 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs -:source: {github}/tree/{branch-or-tag}/ -:samples: {source}/samples -:templates: {source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates +:source: {github}/tree/{branch-or-tag} +:samples: {source}samples +:templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index f85bedf18..9590ac358 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -7,10 +7,10 @@ services that is accurate and readable. Writing high-quality documentation is difficult. One way to ease that difficulty is to use tools that are well-suited to the job. To this end, Spring REST Docs uses http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produces -HTML styled and layed out to suit your needs. +HTML, styled and layed out to suit your needs. Spring REST Docs makes use of snippets produced by tests written with -{spring-framework-docs}#spring-mvc-test-framework[Spring MVC Test]. This test-driven +{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test]. This test-driven approach helps to guarantee the accuracy of your service's documentation. If a snippet is incorrect the test that produces it will fail. From c97efcce2ebdd4303a83431c619f37d6090281a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:41:34 +0100 Subject: [PATCH 0172/1059] Use the Maven Wrapper in the Spring Data REST-based sample Closes gh-146 --- .../build/SampleBuildConfigurer.groovy | 9 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 49519 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + samples/rest-notes-spring-data-rest/mvnw | 234 ++++++++++++++++++ samples/rest-notes-spring-data-rest/mvnw.cmd | 145 +++++++++++ .../main/asciidoc/getting-started-guide.adoc | 2 +- 6 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar create mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties create mode 100755 samples/rest-notes-spring-data-rest/mvnw create mode 100644 samples/rest-notes-spring-data-rest/mvnw.cmd diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 96db02084..7538a6fef 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,10 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - String suffix = File.separatorChar == '/' ? '' : '.bat' - mavenBuild.commandLine = [System.env.MAVEN_HOME ? - "${System.env.MAVEN_HOME}/bin/mvn${suffix}" : "mvn${suffix}", - 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "mvnw.bat" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { @@ -76,6 +73,10 @@ public class SampleBuildConfigurer { return mavenBuild } + private boolean isWindows() { + return File.separatorChar == '\\' + } + private Task createGradleBuild(Project project, Object... dependencies) { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c6feb8bb6f76f2553e266ff8bf8867105154237e GIT binary patch literal 49519 zcmb@tV|1n6wzeBvGe*U>ZQHh;%-Bg)Y}={WHY%yuwkkF%MnzxVwRUS~wY|@J_gP;% z^VfXZ{5793?z><89(^dufT2xlYVOQnYG>@?lA@vQF|UF0&X7tk8BUf?wq2J& zZe&>>paKUg4@;fwk0yeUPvM$yk)=f>TSFFB^a8f|_@mbE#MaBnd5qf6;hXq}c%IeK zn7gB0Kldbedq-vl@2wxJi{$%lufroKUjQLSFmt|<;M8~<5otM5ur#Dgc@ivmwRiYZW(Oco7kb8DWmo|a{coqYMU2raB9r6e9viK6MI3c&%jp05-Tf*O#6@8Ra=egYy01 z-V!G;_omANEvU-8!*>*)lWka9M<+IkNsrsenbXOfLc6qrYe`;lpst;vfs*70$z9UM zq%L>pFCOr$X*|9&3L2h;?VA9-IU*iR6FiGlJ=b~DzE5s^thxXUs4%~*zD#K&k>wZAU8 zpaa!M+Z-zjkfGK15N!&o<3=cgbZV7%ex@j^)Q9V`q^i;Fsbkbe6eHJ;dx{QbdCCs1 zdxq^WxoPsr`eiK3D0Ep}k$ank-0G&+lY!ZHDZBYEx%% z2FyE?Lb0cflLB)kDIj;G=m`^UO<4h(RWdF-DT>p{1J5J90!K!AgC0)?jxPbm$KUjg zJED+#7xQmAmr`(S%BQTV-c97As~r3zD$E;3S)@}p5udA@m6pLgRL5h-;m>LvCq?&Q zokC7Vnk-zBEaa;=Y;6(LJHS>mOJV&%0YfRdUOqbKZy~b z(905jIW0Pg;y`Yv2t+RnDvL4yGEUX*tK)JT6TWn4ik~L)fX#tAV!d8)+A)qWtSjcr z7s|f%f;*%XW!jiRvv9ayj@f&dc|1tKDc{O3BWcLGsn-OYyXRLXEOEwP4k?c`nIut0 z?4S;eO@EoynmkxHq>QpDL1q^wOQxrl))2qya?dk05^5hK? z{P6;WKHUaHw9B0dd&|xw&CYN2fVrn};Gq<=Z^QZk3e~HzzY~JrnPCs0XwMp#B<9Gm zw0?7h#4EY%O-ub6mi&O2vcpIkuM?st;RtEpKSz^Xr#3WHhpsZd!gh|_jGQ`KA30T- zKlz9vgB;pY^}Uh??nQKSzk>2&J+Qi*r3DeX4^$%2ag9^x_YckA-f9p_;8ulh(8j9~ zes{O#{v!m%n^el(VryTF-C%xfJJ$rZj)|Y|8o&))q9CEwg2;Wz&xzyHD=@T_B%b}C z=8G^*4*J4#jUJn{7-3^U(_uUp6E8+GDt#le)nya-Q4kL5ZGiFxT4bF+mX`whcif*? z>CL&Ryn3HHT^^QmWYr<}Q1_Jj7fOh}cS8r+^R#at-CnNl3!1_$96&7nR}gh}))7a0J&z-_eI))+{RCt)r8|7|sV9o01^9nv?aePxMqwPP!x|sNmnn&6{K$K*mVX9lxSAmcqAV1(hKA-=coeTb*otxTOGYXsh zW$31^q7L@<#y~SUYoNKP1JK?4|FQNQb$i8mCG@WhX9i_^;@M2f#!nq7_K*M!4lGz1 z5tfADkO7BZDLgVQ?k7C)f;$eqjHI&zgxhf}x$8^ZEwFfm-qY=+M+fbS)9r8fFE5H9 zv{WPU35cR8%z;(W%5<>y+E&v84J4^Y##N!$B++RI`CZ1i3IW9Nau=*pSxW&^Ov-F> zex=&9XYLVcm1Y?am>2VC`%gMev9$#~; zYwxYvMfeKFsd!OBB@eOb2QNHFcsfKm;&z{OVEUiYmQ}~L@>$Ms@|Ptf3jQO-=Q;1+ zFCw+p+Z3lK_FmIAYnk2V;o915cDM}%Ht5RH%w}P>Yg9{h1mZ}~R6tUII4X7i4-2i% z2Uiw3_uHR!d~5(s;p6btI@-xhAkRg9K|n#}PNT9Dw9P>z$3>30lP1(=mcQ|tpyv3@ ze1qU!69OAx4s7$8r7Y-#5I`m!BXq`f!6C(BtUlG-oq+liqMCS_D@0nSFc%y+N6_Zh zi%L3LhF3zZP{d1)L&SXxPD(fp@T@J;jZeNaf$zl>vAh7=tI z2;wS^QyRdZm~)Ur&!af;8eB8*7(F96K^=WbC$)#TWvB~Awo5AtPf8Il4snD}Xsqd< z>cH+gcg72nTg5tl>oFbwdT{BDyy1=f=4~h~L$)UX;FXa;NdSlyF{(YLrx&VDp`pQI zh3pQtC=d8i1V6yUmFon*LQsNYWen?eO-gSZ4cvYcdEd0klSxcBYw+|5AyCv6TT96h z{7Yh9`h}biU?3oBFn=d8>Hn`1Q*w6rgeX^QbC-WFwjY}Int0;qUny4WMjIee@#0%l z>YAWLVCNo1lp$>9L$Tx`t!dp?>5Pfbhc*!*wzfWkj_x`Q?`3Jc@9r8uq~dgb+lgeh zlA`eUal3e2ZnWQSSYB>qy#85^>j7!=uO-hG5*erp22NaC81#Ytioc>r?D9$b_JiC+ zSp)8KR$%}FjFNRkeE#c5vKbXNJDBoO< z)73Jt7Y|3v45efud1xkg2GO3OwYfsuBV`f6S_D>Aoh2%=`1Y$bHP>0kBvTSowX57H z&1nbbx=IT>X^ScKYL&&{LNq~^UNgR|at`D;SxTYpLvnj_F*bGgNV2tEl1k$ccA&NW zmX(LV*>Op)BOgoric(98mIU)$eUa&jM5bKlnOrHm$p^v@u;W0J)!@XWg+#X=9En(-tiw!l?65rD=zzl(+%<)bI{ZN;SRco{jO;>7 zlSY|TIxuN|d#YHx^^~>iYj2V>cC>wQwWzGVI!6#epjJ6tl_`7tDY17WMKMB@s*Jr& zXOs*@>EwQ6s>M13eZEBJ#q0|;8jao{wK4keesH9?$OSk~_3#*x`8fAzQa7fprQ6(Z zi$}B%m81y*S)RxaX;wW!5{{EDw8)IE3XDRO1Y^%TMr}c|Y>WBAKT=b*K&uMT(?JSl zO>gVtl_bKQ$??TeWr7wYO+Vbl?CTQj?JrW&td`|#@;R2Gca9jq^p`{@)KY97o3}Af zfTh{pUUWD;P7sq=I!lA6;*hq0Nq`F56T)x$K?BMOk}tptYw(%$?*otp2N6IF3#GgqM46Cda!qzvGZcMgcGV`bY5ZIfOB6^;US#WgRai zq#vS8ZqPY953|eFw<-p2Cakx|z#_{4pG}mk{EANI{PnK*CUslvS8whko=OTe13|It z>{O2p=mmanR2-n>LQHaMo}noWCmjFO@7^z~`Y{V>O`@rT{yBS=VXsb}*Pi_zDqM3? zjCZqWR}fEzAkms+Hiq8~qRAFvo}dVW{1gcZ?v&PdX?UG*yS}zT9g7nZ!F1WRH}sHA zJ4~B2Br~8?uhbaX!3g+7=3fVM)q^wEzv**rk5e34==NRCV z3G$G5B!DICFslm)c){oesa_0muLxGoq`xYVNURl*NhE#v2>y9vDz&vJwrB`Q>DhN# zY2GnY!Y^8E%PU0}haXL$8a5QN1-&7NWuC~{62j| z2ozmFyx8GpOzj?&KK1JF28;E8H_p4N^LMm9K0y}!lCxcK79eFGTtGm?7jy?t94Q@X zli|our1#|>f*68fyA0bSn=YisYSl8HB(dFN4Y$qb7p4DR0YQt=^eEMnJkgiM48$>QV6x5*^a|D|t zMPDk}u<^YEYrt|H&hy)DRk%rDIb{LTo;h7=fp^J9Lr&`{9`8_pS*tQ_$KXB$2#5{h z-&yPbN-zInq{7aYZuaItS8-2Mb4OQe2jD*&)0~898E|HlAq`o!M&It@vvnj z_y@))>~_oR%S8OfmFTGYIat^#8_YKMqWLac<^}RZFDcJqvSJa>&6HaLS7p-$)QyL= zHrO|t75`d41Bp37RZtKR%g^%o@9C5Ce=CjuvVQ-KI#Uw2WWa>cho;jztUt~Le*_pT zkfA2iif9QFp;vhd)|A?tdAQ?9o~?EqgL;=)eKFQ{E^u?OIP}fl^5A;$^ZVutCIqj5 z&*i+G?!Px|5~~6zTYf>~uw*kM`5p&Hju&#w!7^An3*mQwTK22wC7p^OsvMjWf`$MY zLX|ZFV#+>Uq2!QyRD9cgbI9nswteMAMWtK(_=d%r?TLrx?_rkjbjI(rbK#T9Gn}J| z5ajow3ZErpw+%}YfVL-q^{r~##xJ^_ux2yO1!LJZXg)>F70STV=&Ruwp&XP^_?$h0 zn>$a?!>N+Kt$UXzg`e+szB}*uw)Z$uL6?>*!0IrE)SgV~#a?Qgg7HuTsu3ncrcs|l z=sQSMtr}S!sQ4SriKg=M`1Y|bC`XJ+J(YT)op!Q);kj0_e)YNVNw8SI|1f%9%X?i5>$lLE(Wfc$wY?(O985d5e*)UPtF!7gG3(Kd z-^=-%-wWCEK`r4oFh^{|;Ci%W^P>K%9dBNDqi%c$Q{iY#(zbwN7~pQI=SHd%WuV7Z zO?0P;Zc6yeN;)IbJIP0=>W)EgE!76jM^?IyQ*D(T})1NGmP z~YAb6T^#R6;)Ls;cV~LWk z33lcLpbSjxStw9Z>Nv&+rPOXxCGB=?ttZs?{OF7;GYlV&w7-82POb$XrogqFpLA2`j&MLZXr=IG>PAFSb2np~x;E_kV{ zsDwbK$?iYRn7$;mHYZhQn6P2#_hXAHd?;q~!Zy}%;@%wT3u|Sa-!WxxOE_fwyFv*Db@>X;Rl+fK1oP?55*dN0#2%SuikZ)y7Kx>`8*9d?}5 zKvXF7J5&Ey6{A8qUFxrFOh<$xdSWV^dw7z|`7RVZJhAwO72V zRrM_3*wI`^ycl7~>6KaCYBr#WGR>}B)Q(V%&$MhVrU>u~ql zjGeZF&>=_ld$oY!V}5}Gb> z*iP38KOav9RHY)0uITwgz99w- zJX-0BGCdY*$c7pi@>@-`2>#>}c(DHaI62ntpKz z`c01Z#u7WuMZ71!jl7hv5|o61+uv5nG?*dffEL~328P5HlKh2&RQ;9X@f>c1x<>v= zZWNSz3Ii~oyAsKCmbd}|$2%ZN&3gc9>(NV=Z4Fnz2F@)PPbx1wwVMsUn=-G=cqE3# zjY{G4OI~2o$|*iuswTg1=hcZK$C=0^rOt-aOwXuxU=*uT?yF00)6sE}ZAZyy*$ZTH zk!P*xILX#5RygHy{k?2((&pRQv9_Ew+wZ>KPho_o1-{~I*s1h8 zBse@ONdkk-8EG?r5qof}lwTxdmmEN|%qw(STW|PFsw1LD!h_Vjo;C4?@h|da4Y;*; zvApQ=T&=jWU39Uz=_yN@Bn0{{)yn8RZ2&X!<*KBv-7tcWdkF1Ij8D0mU zwbcs}0vDaLGd@xx%S_QZ1H)GTt`~>+#z}HXJTl9S!sd9seVJc|_wUMSdD$>k`K_RG zlq(fsnR@KM^;C}}&vG2t+}_nGPuI5ovg$6TYeMPIREGxP@2r~RKd@>gV`mq0XENsh z%IRZ-ZNP+4#J`o-yRpP;w@;CrSr3wiix3e9Qc|s(WapRq950P->g|JYC$A)$YrGeH zz5dKlAHAPJ>%?llqqB&#+#VU3sp=9>Xms1J;tSYN>LMwNtU68yr!})K4X>%^IrIDp z>SHy&6fJHybwS^BW>okFeaQp6wxaVP`hy;ZX#e+=w3c?PGD&_LmeqL8oZ*YaM1+#S z5WNAKo4+99JW(+qcMjh;+c%R#R?t;(aQ`2`C=bo((ERzgAwKKazXy*0wHN;v;P|f> zBW&?`h#_I^?Bc5GX7XP@|MOiw%&-#?EQ|w+FdCl_&qPN&s$|Z17UCF9oXS#N z)px6>zm&}0osTnCGI;AXsj`q=LpIsW4x}q~70uey5N_NpdJ*Gv^@$g@f2{EB>LP7Y zE5P`jZh1vHNgk7LfMT({jLCjRZa4ubW;UA#%<@Zj?efrPdm{W3J5UEFgm`YkVqz;AMFetZuM5uQpvORb1GDX`WZGwTrF z46+&sAri5QXCfGYpdgonWR5`>ZEa;?jrKvfNvXF<&l)1uU-3q#4X16R2~?P0yg3H` zfw82QWZo^cac+%(g^_6`+2>~Fvy{pOCGnj86+=-!N`GPWAjus1ejhn6f4|mDkU6EE z&u~;xfdRMkj=h;4d~~+4(>L8weT3cz9e@E11EH!tX<IC!@kS+dsIQA`HQ2vdoS zzSD0U?mb1M0@qXu{yhZk2Y6}2B-AvvYg|tRr6z*_*2l*VLiR6G;M{O^Znq~LI%=I_ zCEU{htx&Bo+69G`p|A@R>KlY1*;;!{aWq?Pc0Cu!mT-0S`!>3<@s%Ri;utYNQ+CXDj+LC5<*$4*$-mogGg^S~3JRv{ry zPJzKJg!XKb>P}yJVc^1V@T&MV{z;@DLhvV{dG?RogCcPkROivliSr58>5Zw&&A2?n z9`JOLU;eQGaOr6GB(u{t3!+$NaLge$x#M&*sg!J;m~rRc)Ij5|?KX_4WiM-eE%t8e zqUM7eZ~ZonavR;K4g2t$4Fj=UVyEHM7LPb%8#0?Ks{~?!qhx9)2^>rg8{0npLtFKR zJB)19TFiD^T7IUXA8wt!@n5gj&@OK~EO}MR6^qd?^-?%-0~b2K9RWh+_mSEQQWsLCFOt#JlAQMgNxvv-m z;sF*r;WZ*Wi@I|6pMN+|_rLYKlWwvpKZY9rA;fo8l8hFQGI?4#kt1-r4UL;nPF@{~ z2T~a@2>yD|GuU55boxoIIe_BFo2Vq&rs&2itv|B>OC*bIeOqMBRw~y5KRMwiVHc)` zIBdliiY?Ai7*+k#NZf3MW5!hya~RZ6r7k)b?HF0e(n`ZX=iCpT7St`FDwL@SGgKlq zNnnU*3IcnYDzJg{7V$cb`xeb4(s(({&%f69XMTw-JQErS%?X_}?&y&tvHw@>1v{#R z4J@(=el^kRI+jGa;4)l#v%-jM^$~0ulxh6-{w*4Lsa>Tuc z>ElR3uM~GUChI)c{TW${73A3$vs<&iH;e?4HjW2MvSz9tp9@69+`_@x{Qte^eFo5IlAi&zw$=t6u8K%8JtjRI88PFNM7R>DaCO3rgngmk zI-RMOyt@kr-gVra=tl^@J#tI7M$dird(?aU!`&1xcm~2;dHN(RCxh4H((f|orQ!BS zu;(3Vn+^doXaqlhnjBJj-)w?5{;EEZTMx+?G>Rp4U^g<_yw_blAkdbj=5YrNhZB9@ zNmW=-!yFx5?5aF^+6*1XI|s3lIn_eyh`uv%?liNzSC#z&z^R(mqEYL@TdWzgkf>g1 zedzs*={eJavn{8vF%4nf@et<@wkOPR>NiVuYtESbFXQ;sDz_;|ITVeoW|me5>jN5P z5--{13JT{3ktkAf9M;Jty)yectg#{+9sK{C;2CvPU81tB3{8S5>hK{EXdVe?fR?sd8m`V zPM*$)g$HKp0~9Xf6#z!YJ&g!%VkCMxkt>ofE!62?#-&%|95^)JJ9 zk;GlJdoH0HwtDF(_aTv}mt$?EyRyE6@pm5DG~Gj-2%3HcZT13e)$)z99bdK_WCx|Q zQNza(R)Z>ZKTn8oIdcw%c^pFaMpFZ4HOds!BODgSBWJJYW3I_WJvoEm4xsfs%#LZ6 zdPCk{5XJ>2f7Hj-i*9lTW6BKCIuy)3L!b3(uPoSgW1WA+OEYYBRgSsJq7wjHh%c8ymMs3FU%~cprqL*084p*^T3{J%Gwq`jB30n(&y6- zII8-_r-s5&CVtsoNZ9%On?7yn;oZG03-$wx^uRk9>b*ufh15|HHk|%=MA^ioyb9CYU$7y$4R|M5HvpiCTxKSU`LUg$+ zB3IBl&{qO}agqF~BFM6&11wMeR-#Rkuh_(^j+P4{;X_w|siva$5P`dykyhfAUD%e8 z+{G0|7(Q`_U91sMKFO^rHoCWfXi0$^ev)-187G}klYv@+Rf%uZ&T4-Uhh=)pcU6O1 znXc^c5)!$X+39|4`yNHuCj0wkm+K1VN0G3_EL?-ZH$p5Y*v6ec4MV zS~1~}ZUhl&i^4`Fa|zyH4I%rXp;D6{&@*^TPEX2;4aI$}H@*ROEyFfe^RZI%;T>X> z>WVSUmx@2gGBxkV&nfyPK=JI$HxRKUv(-*xA_C;lDxT|PgX*&YYdkrd5-*3E1OSXBs>35DLsHHp%zm+n0N(Yu{lMo>_t&d1Xy zfCxl=(CNNx>ze+7w)60mp>(M``Qn$aUrVb$cJAb6=Do7VgW`Qn2;v5{9tB)jP$_mB zn{Hb_sMs4yxK|!`PI7+zO68}{Iv)dpu!+ZZl)xuoVU(oFsm<3gT{j2c*ORl|Lt+?dR^M?0 znW6rNA)cR*ci;z?BaG(f(XynY_y+kTjj~T$9{N{>ITQ4-DmZ6{cOkoea9*LpYL{Apo0hSpLqJu z9`tjP&ei;%pn9QY>-$9=<73M#X;qGb+%Bt0x>=u`eDtthI+LWB9CdAO=ulZo9&Ohs2X8GW>b7#&U|py28KTvPBl#Nqv^{AgkVXrOyS z@%3)}$I&mJOYWoG$BBb)Kb~0ptDmBxHNH^i6B8FA7NR2HfTnjP?eDnoY4NS_aYg4P zGGPw11sAf^^fTkY#j@T#6Ll*^GVaPo-1;aS6_a}{r{tWZilzse2m zc?LS=B|EWxCD|!O%|%t3C@Rd7=rKJRsteAWRoDu|*Kx-QwYZQeYpGrZ_1J%mFM;*S*u=0 z%1OC9>kmCGqBBu#-1jVPRVW*BTv%3uPI8fO?JOZD#P_W^V+K7&KVB>hzZ@PdY*%Ezo;}|5Mk`Mo2m*_K%no*jDJGp(s9j;&U`Z>z zO#SEe)k!p$VE-j2xDoX$!;Up5%8x$c`GH$l+gTA*YQaE0jwCOA<*__2NkV){z_u2=4NQ zSk$(oj$%ygio?3V8T3IyGMYvPs`t{im2IoHs7or+>>MYvG%Q?PwOLqe%73uGh6Wn; zo>e7qI$9?%cVVkvQLOLKcU5n*`~qn8pzkdu=Z4#2VnhUy>S*;kT=NqA!dQtnE?wVg zOKobxJ|QCjk`!(2*~5NQx{{=Lr=)ndyn{V|&PxUa=xQXVU?#M24F8H%C*uvs(#Va0 zSkp}0EFYq0#9xp&$O?gIInc#^^_6Ol88W%)S5A@HeE0(SR&!Yl>u=*5JEoUViDR@2 zJBjTsp=Y44W`Nb2+*CcZCkwP(QChX1s)b09DEIZCKt1$q2~;&DJ9!{bQ1Y6&T_9u1 zZM8^im8Wf#FUO6tZqc7#`z0cN_JA>#U_b7he%?cCnlV2&47y5Fc)Z7bp5xGe1zNq9 zl1VaV-tsm3fY=oIX^SPl!P;9$o?**0brq#ShM~3CXhh^SK0oOKB9O>;q3G@ z&4&h$mLSgohc^5IC|H>IGfZvVQFUT>T$|U7{znY`56<5d)07oiv*2R0+-BGPPkWJ! zIOzKF+<5o2YLWP|SGCx8w@<>u6K1o`++xJ+6kaJrt<&0Haq zyUccgxI$sR07Vo9-pF);heBva;?&NcAzC*gSSG9B3c?A;IH9J zl$j%F4*8;F0;H2Cjo*kWz4{kSh?nX}23&&KL+U(#nOAuR`wn@uwUNkWEgb*ZShKPy z`aXTJT4f*Um4`iv2KOfzf-~`#pOfH8>is*xnLBDTyx2Xuc8Y2Od6z((P2AZK@b_96 z#0V6jdw>sEDJ#uNGV|EshD1g&bYZCzCZTZ)286HLHc8Eyy_HPi;d#%;Wx}d6tUUxq z_VB$+898z_{9-A<*v6VI7?(dC04o!8$>DQ$OdbrA_@<6auiBNp{Dw$Hs@@gcybIQT zAU7Pc5YEX&&9IZ~iDo&V`&8K$-4o$)g?wF8xdv1I8-n}1bc7tviIBqt z#iIl1Hn;W?>2&#bU#VZ1wxq(7z=Q15#0yoz)#|r`KSPKI-{aN%l61^?B4RMDt?Vk` z)G#K6vUN?C!t{Q<@O4$0(qI>$U@@TI2FVF;AhSSb5}LtXx&=k&8%MWM3wv;Xq0p~W z#ZX;QFv5G9-i6=+d;R7Dwi)ciIZ1_V!aw;K^etau+g0fOA2HXpV#LQZGzf?h#@}(o z|3w!sZ|&mp$;tmDiO=zef5C|Alz+@@4u5#yZ7yNpP=&`432%a{K#{;nsS!jwk-$Qs zZRty}+N`Y~)c8|$&ra{bOQWM2K7qa}4Y{ndK%dKp&{ zFCvX{PAy_C{xzS_-`0>JlPP7&5!5 zBQ$NQz^z#2y-VeIxnfY|RzU`w+1t6vwQ|wM)LlpuaUzYehGII;>2DYyR|~wC@l97s zgX=f*1qtfDyco%BHmN+o<2qoi`D67R+RM$$NN5-moE4kx3MCFfuip*45nComOZKQf z3!(8tkSdhY5+A%@Y=eVEZkXU3S6B2V-R$ZuRIXWhsrJg3g)p4vXY@RV60bKuG zT6T!enE<;(A{*HPQhae*(@_!maV~AWD4EOwq10tkCXq+HPoe_Pu?d4Kg=2ypcs?&f zLa>mEmPF4ucJ%i~fEsNIa{QmQU27%Abh|w(`q)s~He5$5WYQ_wNJX6Qop<=7;I1jd zNZak`}0lVm+^O!i;|Lwo}ofXuJ)*UtH4xaPm*R7?YS*<&D__=@Kki>{f_Z-XqM;Tj195+~@d;rx zh5pj8oMuupWa#E(%85**I~1Zat-Sa^_R11-CiKdd`8m(DGuzOm9lX$Dd!DX!_Al}d zS!-|}dWG80S;`jSKDH%Uv;-OJNeBI0Bp$z->{_>1KU%h&Af7nns(L=xRN1 zLvOP=*UWIr)_5G2+fCsUV7mV|D>-~_VnvZ3_>=9 z_bL6`eK%W*9eJ34&Puz^@^ZIyoF@%DTun#OOEdUEn8>N9q(}?5*?`o?!_<(i%yc`k zf!xXD6SQscHgPgiHt>x6{n{+}%azrfV4VHi#umyi0;11c816`E??2`$;Rc`)qA2H( z5L|{o=ut7Te=^~@cR0_#cah0?w0Me$&>}ga8xxy=?DDl#}S~Y z4o2n`%IyGjQEP%8qS|v(kFK&RCJbF1gsRVJ>ceSjU`LuYJu%C>SRV#l`)ShD&KKzv ztD<9l0lcW0UQ8xjv|1NXRrCZhZh3JFX_BNT@V|u9$o~8M=cjOX|5iBS|9PAGPvQLc z6sA~BTM(~!c&V=5<}ZIx}O7A;|&bd7vR_y)t+ z?Vm7kb^gJ88g;!fRfMTSvKaPozQz4WcYD8l#0WxQ${P%0A$pwhjXzyA0ZzErH{1@M z22-6b1SQ!SMNyqj_7MXE2cwcEm)W)YwB)ji`3Y^5ABx--A11WB3mBQB<7K!~``j&@ z8PKJ^KSa>#M(rar$h}aBFuNI9sB5uAquDlzKW+hYB&WKf9i&+q$j5P;sz2u$f`uHS zaX8$!@N2b81<<0w<{CpXzQGqSZRpfVb3R%bjsw-Kl}2UH>}1M?MLA#ojYaagiYL!P z$_@7yOl~PbidzJ8yx{Jz9&4NS99(R5R&lf~X_{xjXj|tuvPgvzbyC}#ABy^+H+FN0 z8p5U!{kxOvdv3fr35|Kb`J(eXzo*GvF6`_5GI)&6EW}&OGp=!8n`W0mr_o~Xq-t?% z_pDDfIW#L^DmX?q#mA%Jz-f86KG`^7V|1zdA#4#<=}91g$#@J`gOqMu+7H&yMdNIt zp02(*8z*i{Zu;#S#uP#q!6oNjQzC|?>fgzorE(d+S#iv4$if+$-4$8&eo zuSZJ1>R2HJ^3T9dr{tn+#JMGv#x@&C$EZapW9)uhp0`rDsISKrv`~3j)08JZlP&}HwA!z^~-?Ma(x0_AS{@r z8!(Z}5d8+5f7`r3pw_a=Z`!0r6r4%OAGYBoq3T7^xI@9xG3prNo>`}k>@VAQk>(=DIy(szD&6@u?YVdC|pJLT@lx{=IZ; zIkO4)YWp*Dpp$`H$Ok#yf;yBmHvTb@)4j)jVNF-O?$nD25z7)I!cWQ|Yt zeS<_C{i|BS4HICD=}T(|)@vd(v!?P4t4>APo7`K5RJvcTpr_KgWeB~zMLknrKMgpx zyN-EI%es5e)FNho=}qGu$`98v(QDPUMUGrY4tq>?x$md>qgNO0@aAQLMLr8XD8z%; z2Osn1D>N^22w4Xb8{~fi^i~SthAo7%ZjNb)ikgj0_AsXqF_0+W6E_doOUi0uV6Lvg z98Xk#>IK|-YHx!XV64==b(nYKMEyqPF?D)yxE=~;LS?LI_0)|1!T3ZtLa?(qd|YlXdI-e$W z(3J*FbOe3cSXvDaTHU^Hqpf2i8aH+ZzqY$cFFIH;fxMtW^(AmiMkBtb9esujw?rte zoo&0%Afb~VBn6A1@R1!OFJ0)6)Fn72x{}7n z+b#5gMommvlyz7c@XE`{ zXj(%~zhQne`$UZ5#&JH0g={XdiEKUyUZwIMH1rZTl%r@(dsvBg5PwEk^<+f_Yd~a@ z%+u%0@?lPzTD>!bR(}RQoc>?JwI|dTEmoL`T?7B zYl^`d{9)rW)|4&_Uc3J=RW25@?ygT$C4l-nsr+B0>HjK~{|+nFYWkm77qP!iX}31a z^$Mj&DlEuh+s(y*%1DHpDT`(sv4|FUgw5IwR_k{lz0o=zIzuCNz|(LMNJwongUHy#|&`T5_TnHLo4d+5bE zo*yU%b=5~wR@CN3YB0To^mV?3SuD~%_?Q{LQ+U){I8r*?&}iWNtji=w&GuF9t~=Q2 z$1cFAw1BTAh23~s$Ht$w!S2!8I;ONwQnAJ;-P4$qOx-7&)dWgIoy-8{>qC8LE?LhJ zR-L4qCha@z*X+j|V<+C(v)-UZmK0CYB?5`xkI)g2KgKl-q&7(tjcrhp5ZaBma4wAd zn`{j>KNPG>Q$xr7zxX}iRo=M#@?>}?F`Sv+j6>G9tN!g@14LUf(YfA4e=z+4f zNpL4g?eJK`S${tcfA{wbn({8i+$wMaLhSJo`-Yp@G2i0Yq~@wdyFxoVH$w9{5Ql2t zFdKG?0$ zV7nmYC@PSsDhnELrvd8}+T=C6ZcR?`uapdWLc2eaww5vKtjQQgbvEr^)ga?IF;@1(?PAE8Xx5`Ej&qg|)5L}yQA1<^}Y zp7WZpk%}L9gMMyB^(mFrl&2Ng$@#Ox3@Z6r%eJ`sGDQbT0a9ruO`T|71C;oCFwTVT zaTnu)eVKURM`1QuvrBhj;1e>1TEZW54sKUfx0Z=N*;Jpdh~Aj-3WB zR|EYVGDxSvnjeA?xxGF41Wj?~loVahklw|zJ=v3pOEVZFJG^TvR z-tJN5m;wZp!E7=z;5J*Oaq%2bc|Jw!{|O+*sja+B(0D2_X`c2)nVkzP1S~LOj~xs!@>aN z3$K2^pW}@R-70K!X&s4DHHoV&BmGWTG4vi9P1H$JxmD|t_V{GlHZv(`yJ234IVuSr z~!;~#ublS8qdL8SJG@XRCwWhkZyg_EKH(sB2}QQSv4W}|CT0ntD_4Eyp519d1%yKvc33|`yW9QzeJ4*XLP7@l=td+bwxSL~jCf-ny)IDC^~u5s)E-y^FdtU?)hkN{82Y{Lo)bCWcBOx;Jbw;)Pg9bWQQTY-3RWehpok!>D>Sa2EcEOS@ua)#G3I+GxL_ra^92Y!}tMX zwAp*Fv-aAarn`ME7N#Uyim%ynre6u?KS15L#$#rKZSgLnXx;g8TP9suMpO055p278 z%o-6eT(3gdIVFN}Gb3k$zbTyrHYel1x6OxETsk&h0E?&}KUA4>2mi0len7~*;{Io~ znf+tX?|;&u^`Bk-KYtx6Rb6!y7F)kP<5OGX(;)+Re0Y;asCLP;3yO#p>BRy*>lC$}LiEEUGJHB!a=&3CddUu?Qw>{{zm)83wYRy%i}UV2s| z9e>ZXHzuMV#R1yJZato0-F|Jl_w2sUjAw@FzM=DxH}vM>dlB&bQ!>51aGc}&WAH`b z6M6iG$AyJIAJ7-c0+(;pf=2=!B=%yoM1i9r==Q+}CK3uW%##U1rP~mwjUb8PLsi8Q zq!aTLLYK4HQ$vN1sU;d3XW{oFA{u@1$tduWmdOqc(~AqWq+`V)G&?YOOwAK20x>{q zOgII2&A_FXPzVtgrD80Y5J+_SEmyUcdM2N%q);|ZF_m z)6PBcOcAAy3kN*`8ac%zPH3^61_zn6_2FT#NCOWYx>ezqZzCC;tzM%pJC^gFAFcTs ze6C3WE-a*=nt8tErPG9zfPRn$QHqB7aHe8x3w&rWT(0F54<2uBJDYtbB}y|@9V6T( zmM!t}T5SuwxyTCma14&l|yiQRw5Pn|OiDBkx z?4tUGrIVsC9zs=F{W>zl9XeknEc+~Mz7zCnefUPUF8iF?A)QJK8=84#-TLLxq?BTM z=VYjYW%TOhrBp>3D@K{vStlEUt%e{HRc=766AQ+s7V_F|1A!)P3?y*=gUgbZO;O39 zX*BC((-XbnoaRGxxhRQRVKCDG9|qC6?7TwCz{A{OZp$Wu(~0DFo(w^P3f>4gr8@P^ zl8`!vA=_fvwTZc%-Z42}m>Q;KQ~&v;ipZzbA2;}Peg*v}TlKRmU%4WNN<%qb!cLo= zoSx;XBrv4}ErykT!)z)Qar4o?(q6!mpWLNFe~Nz0S@yI{1)Lxt<0K=Q$~>*HH+Wbp zQ~fx0aup_lZb|e6*@IJOJjw~Ypiwdq69&Y2vthfGq6u1!Joy%;v;~4`B@B*S(}}i- zmZc^*aHOK(dd(geOKg)P+J4+*eThk;P@wRjvm}e)h|#EpsV9YoqqRW{)ABhRlvGA* zL$&k5w*_-X1ITCwXiH=)=5lzjxY5tQJTBrv<{dM7$98pdK%i;RGZtiJKaSGCji7w)aNrHu_9_IPGHS-mMN5AheTn_ia^YdunCzcp2ap8eI-RQEm zj(q7_CT)o|w_noPm@MVqIjv%H4Bdo6*9*!Zj)bLx!p9POp(`$dj1QW`V=;=|`Gx8QST=OnK5jlJX3!KBz>v7j$&5b5YrhIArRVL)1C^o{@DJ}*mk*s=< zDK{e2f%fG)mK_Mz*x@#ahOO)cQQ#VH+8Wef>NKWcu4J>PIc3iz8y6PwCmY|UQ(O3!B;HtsE&jvyv^XjL7Env5#i zH4-k5GzPr-%36#%+Hvw1*UiOIk3b7F^|1dPi!-i7C^ZWp~_KI%D!sGYb@@zXa?*{XfjZ~%Y^mT!kaK_>K8 z_jL78^ zS0eRdqZ0v~WWow1CE;vDBh#{w9R4JgB!})W9N{{D=p-RMnehZ#pH*ABzDP46ryZkt z4ek|LHS{CDhTTMQa3a5fO9OLg?y$+#Gi2}Fv>QD-+ZEQKX2Fv{jr~miXz1ZpPcXvJ zNvQT@kQbBz_Y4Kg)*`E2t;tPh5_7tSGvL-|-A`lgHX3uVG4jLev9>YCZUeNNzioL? z;OBD{z+=Gs3+*ph)#bO#7IHl|rOFfvpK%cF>W??Q!Nh&B@hByD&}g|>a?GJ4uhX3g zPJXKKAh&zWv&wITO66G{PuGLsxpWSqaadFsv>_vQt?LVslVob7wylsa+O`IYWySoO z$tw#v7=&7ZGZqS}N!c##5-bC%>ze*s0H9J%d|!JgE#uZ|k1_bAn*x(Y%r{c=(HLwNkPZOUT#@j4{YfG#@=49YJ{?7? zddbK}G-@Dod&^Vf`GOo)G|`n@kq?Z=o84x{889+?F*dQz(kr@9lQ-TXhGN`)^-Li1 zb}xO2W(FvB2)EA;%qAkHbDd&#h`iW06N1LYz%)9;A&A25joc!4x+4%D@w1R+doLs= z#@(A@oWJq?1*oT>$+4=V=UnuMvEk;IcEnp4kcC<_>x=Hw9~h+03Og7#DK(3y3ohIp z-gQ$-RQIJTx%0o@PDST|NW41VgAR?CH`Sj-OTS0)?Y*M_wo|92;Oz)aya`^I0@?S{ z<%^epAw!Tw(bvSmU_k~Im^%#|0`Xkcmxj;31jX2Gg?PbzdXp9Dg~P)PW+Xi%iWiCr zV-Vv9IR5guDS2lGV!lfTWxkD8w%yz=UB`2j2Zb0eg~arRA*Q6>`q=8#4&OC|L6O}8 z)!w(idG0yk-BF#~k@Avk>an9z_ibOP*Rb;db_PsakNWYdNoygT?yRG=+5>ud<6Vxhk?P9rk!+8?xMg!x5kD*f2XOd^`O3U zlO;ImEy0SYI_J05cMW{dk@%d@iZFCNhIVtOm8$viM>=zM+EKJG%c0)dZ0D$4*-psQ zW+Fq|WmbYkBh5|^-l$w-`Uy8#T#<+3=}z!(6RadEpFlr1f6OFuQ5sG735YicWaoYR z`wuEZT2dntHGC7G*Kzk$tsm?Fd25LTHJj?Zo2RH;9rW9WY1`;@t_O3NC};dayX;Ib zgq6afb4!50qL-o5%yzgcR-1Xm-l4SE!rE>o!L=E`Jeug(IoZ36piq6d)aek0AV)EJ zaha2uBM!>RkZHRN0#w07A=yf4(DBmy(IN6NdGe$?(7h?5H)*?(Li#GjB!M{nq@C3# z^y{4CK_XQKuO>(88PRb&&8LbRDW1Ib>gl6qu(7g}zSkf<8=nFPXE1~pvmOT3pn^sa z+6oK0Bn$TBMWYTmhJzk_6)$>>W)nF^N$ld9 z8f^Y^MLVz@5b}F0fZID^9%hRL#()Xw*%yhs&~|PK|MGI8zuO!f!FqbmX9icd zXU(JOCwac|Z|=Yr(>Q3)HsXl!^$8VSzsgI#)D2XkpZ2=WOBcFF!2&d;*nF%h0I!`mRHl$91jYzqtLfNHUoYzrMzjR)u zP_|Hti4^){G?Ge6L_T^zVdS@KHwtq^+*+aBNl=hVc6#KB-It()qb&8LhnVW9Yxn&S z&^s^u1OzB(d_ByXz=xm4cpJzNzV+Txh`~H(176n4RGlY6( zg?ed(a!J?4(oL}@UfBpgPL*)KrGtM_hMIdu!RywK@d!b-{YAY?(?w3yB@Fi3g|G)| zho%)<=%Q$Lo7S-BxEjTL;M74{y+`Q^Xg#j}VvF|Y>X7s+Ps~aqT--tJNd9U6;Ej&o zj@|!`{Xy90t_Zdb>+m8tCFJ@X(Y$mR>%)gv4Vt;oGr`idhQ7H1^L3v4<_2}-UoguorcscRfdgumUVa0mK7-Wm~#vbrnX9ro}@82q=9t;lM9nH<} zLL#=1L7*f+mQWfyFnETMi*fe8AI+gdY6BM7CkRS&i4$ZRv$v*=*`oo>TjZ84sYD&T zI!DgZ4ueeJKvjBAmHNu|A?R2>?p{kQCRy zRnGg@C%oB#-;H-o-n##G`wcPWhTviRCjB{?mR20|wE9Kn3m6(%Sf_oNXWP^b;dz7( zb{blETKwpl`AT#W7E6T|0*bl?%r{}-BYdwrn0zN(DZXM1~53hGjjP9xzr$p z>ZH?35!~7LHiD7yo7-zzH18eTSAZjW>7-q5TYzDvJ$$S$Z@q)h)ZnY(3YBl+_ZK~* zd6T1UEKdrzmv2xc>eFj2^eQPu;gqBdB@TLqWgPk|#WAS0c@!t08Ph)b>F3 zGP}9_Pfp;kelV05nUfnb%*Oa{h;3Yi^B5xyDM~1r@o%v#RYi-%EYfSYY&02eW#bGb zu8(H8i9zhyn%?kx5Txx^6 z2i}CK(HeQ_R2_u?PFp#6CK zjr}k8Cx#C?DFgP`uN<;}x*Gd$-JgG3J_i3s>fk@_Po}b|JNz=Dm+<{^51m=mO;n4B&azYm{>+VhB{iyxuW+j>w@>VHcJyoSBQi=hu0;p zPw3Aj?%Ai^UeD{ySPIqsf|v0L&f_fmE7oh(s|jwbkK5^AQ9F|;a5V}EdSE?fyxdgf zHTq!f0;+-V{0oF+l_~>rMGk?f~m^wDXlxqt1@+)6Zv?BNR$+%$i z*NF93f}~4d9H2C7@?IibyqUtLL!XZW2ap4fkkxMqDZuZ>`+AfWJQ%~O2WR}NoA=OP zieg@q!mP z?=qU=EE6L0_UpzXt0qwX2tF~}c|;`#MUY2TMz6k({hpkiSz>Dxt*4-PtkAdAA*0hn zk~CK6#V=*^m5 zg$tB6rSO-=9l>GAl^DjJBHdk0wD0(L!OrcZ?qmtYbl+}s(@rtE-O=RTx*1cZq~u~5 zQPVt(IB=*?Pm;Le%#i1SFxHY|>=Y$^RF-FGAUSkBpn`|+p!4RHyv-Q(XgZ5Xg5W}J z8RcT?+4FdVQ>z~9kP5By8eM95f_LDnsnA%K;i6`OpcuJS=^n|6nH-B2EhH=dLbO@Z zuw=Ug>7gsu33`Pzy3Lji0x8OCH={?VRqFEi;@oDIS<*?dG@9X1*tlYCm4YUIMhyfo zJ~=K@-X$D z<-4dH<-5o#yMj%f@U{nfWYVdrREJ}_o4&|c*_+M6gk z-Up9-i~jM-bwR;Bf0&C5wteli>r7ZjGi+mHk3aC4mS5 zPC^{w+G%menlWun+&<#i&DJ41thvk;OKZEB`S%sZ6 zzYpO2x_Ce@fa0LuIeC=7gRHN#os!MQ7h}m9k3@u68K2$&;_mSe2`>uvV<`RgC)TKX z`J}&Kb%*f{Oznj$%-QafB}Zb$Pi%@D&^ZTcgJ0+Bk6-iOJ-P|Q10)5ie2u0JzKb2r z2C@{f?ZBcPw5%h&aKG+6%Qvhw(t1Y{hZ82YE4(Tlk`2VCgE&1x;AUt+5U*$%>P|iWLeb_PJL!VX=b4#>#QM;TGjFHBNRy+d{v>2cVXFyqaLd300 zFHWrc8lB1KSOH3dkJClJ%A5oE^31WrQZ3^-3`Zk?1GqoV7Wr62=V9C=(;#R zhzXAT03)d z9OdZ|;CjSnqQeqF-CUNR=x9x76JYnpr|T+6u#$y=7cMVG72k4f*BJIG>l1NNvyv6NQzr4U`r;= z&%W1Ri2sI5p|8%q5~zM-AMptHj_eX7FzJN7t(%+2dA)efyFbePBsClxY_yMqWbEdT z+jm?SZgH3mCzU?e^psnyd8UK zfZ$^_^}C1WYB1-$m4qwT@#=wsAq$9Xj=%IRvc#V?1azEi|RSc;M zQn;3%Gjk3D)R+3`gZplB>Pt;g?#EiwRzxON;% z#P5IK*YAh1Md<$o21R}j^8Y#t#`fP`nErnb@&CkI{`XNXulcVIXwLcS%VE4i4-!8a zpj-q)#TqXkFg&z4G9pG45A-$B_Lfacr)H85ge*yqTLAb(oY1$6Xu7Rc%^aVOmzsKd z=WEXA40~hm@7FKD9t14nSRt)m0XWkP1YbAE009nIupf`md=v&J;C}estaY0%^Z;;lf>5AF-y%Xf1QEK(}4n+ zhKsTx^bQSpwM=UWd3WRcpEQfw>P%zuhLeEdY}s%cGitMZa14Ui*Mzm%=(7<#b2gHmJ?kdeymT7H+Z8k8tgd zp-dhC)R!P!)w(n%RgOi%^)LGZX)yxC%@f@d4x@IRbq{elrCHyIuphEE6qd6l6O`;B zi0WQg;j`hcu51uYTBSSYNvY{Lkn$iu=Ae0g6o1cSTRwXmEvNcNI zv;)Z_?g>?aG`Zp}*gY8%LGI}{>J#`x;v=*ykuY@z2Erz>@b*)tMp2>=C20MI8|{Z2 z9hbyDJ7d#MdWK&fyZB>Jdm!#x_uRw%>`OuM!&QMim}baa76{L|VAuq%1UpXVHsClm zPD4}hjj{lj`)aaD;x|PJ9v@?8gZ!t5hER6!b~HJ_l9P|(h&R6js3mAfrC|c+fcH^1 zPF*w*_~+k%_~6|eE;-x}zc%qi-D-UpTcAg|5@FCEbYw6FhECLo+mVn^>@s-RqkhuDbDmM~lo<4sa`|9|$AltN_;g>$|B}Qs zpWVSnKNq69{}?|I`EOT~owb>vzQg|?@OEL`xKtkxLeMnWZ@ejqjJ%orYIs!jq3 zTfqdNelN8sLy2|MAkv`bxx`RN?4Dq{EIvjMbjI57d*`pO?Ns{7jxNsbUp=rF$GCut z7#7Dm#Gvh}E8~2Tyhj2reA%=ji|G6yr%@QV{(90cE{JYOW$0F|2MO+TM^`cAu$B7s zmBV^{IqUIbw5~muv}st`dDdIxSU@Eb>xf3$qwEcg;H+vp1^ArN@A)RtQ4hrid2B{9 zb~pG8?SC3#xctpJXWRGXt=cx6Cw!IqoJrK)kuLL&`UYYB{R6Dw)k9nKy>R#q_X|V* z%zVsST$=d(HozVBc|=9<175^~M$v$hL9azT^)TL7BIA#qt>N2^iWvMQgt;!YZt~cv zn!x^OB!3mOVj>^^{mloGiJhLI4qy3Vt-148>9j~d8coH)q|Cg5P89Xj>>hjtzq5iT z%go41Nhi}x7ZztTWj|deVpj>Oc#IrI{NxIm;qhnuNlvNZ0}d=DVa}=H0}Vi-I+wKK z*1uD=0_)b-!9S^5#(%_>3jcS-mv^;yFtq$1)!wGk2QP%=EbpoW++nvbFgbun1Eqri z<%yp)iPo|>^$*IHm@*O74Jve%nSmDeNGrZ&)N9 z)1rSz4ib+_{4ss2rSXRiDy zgh(descvk^&W|y)Oj#V@#)C658!**J#=ckpxGniX#zs0tA~NG>E#Hn3Q3wdKBfMG& zK}2y#|FLt}E`UQ6t3jK#G&e22bMBc3=C)LyqU706frdCAqa;~Q0L5)KJ4?@h*FFu4 z!s=hOC;G?Q)BRKJ1q_XJ9W5LLejp1L*187&5Bo4Of)k>T=WpQl3v#4iX$574fW`p+ z3m}r-F8Gjv1m3yTia=+2An1+E&psbXKjH2{<1xMb37`|D<%7c`0`~m0r>AQD^%nUJ`%PxS>)*{i zg?VHw)ju!$@$>xGszUyM_BsCF3*%>rxVZ8vrYB?PvDBBHQWz04T&UpxKU7{ zrb~8R4W>e)){FrKo^O5ts8O^r^t70=!se(2-(8&aTdaFU2;SR=dyECLBp|MVU@JIt z)z$TAHMKRnyX*5;O<*xm+(>Fo41G;Tk0w01ilh#uFJa{teQne`QCOHZp`&du5gkAWr@9Ywz%@P@KB0bD{lXo7PmrPC%J!A z%orlB>F}qRa$`XC2Ai_4L56#h2GWm;>sScPxhMO5a*guk2 z+56H}PZnq-sxASPn!B~W#8B1W=OQPf-lEbhOh%>%{AND;w%w;t<8%a%HNk`LQ0GpT z6au2l)=Brql2Fq{Kw316jHdW-WF<{46(Xad0uxi%3aEARVi*dKaR^jjW)$<$7QEiF z0uK-~dQ@|hxT5M|t$pBl+9IJig2o;?4>qY%<|sZ4Rk0Dc{ud;zd`g$&UcwLjY))aV z4jh&lc(;hjQaWB)K9EB@b^I)LQ~N_;SFEEWA&}`)g!E7-wzF%J8)yZaSOeR=igBiM zaU=T>5*oyz3jYaqv-RSC;r$%d^Z(cbLGwTQiT+3KCMt*OBOD@rPZ}8;)1_*l<5aBp zjl{A?HiE$Y6$NWUgPY(x@k^9)A|CC#nqZ?B&q-ceGE;Y7F{@0{lQuPnsj0~YX(VoZ zdJ})6X8821kH4_0vt$gocDeSve(SuROm_bM98&+q72$1m(x?A;;)@TWyuVXQV!{#( z41CN;(vq_a|56Yny*sb>5`lt+>?dvF0++3L!wQ_eJmXi)z_1UAmNi80_bG^|J$GZs zK^|0X@8jq9pyPt$dpiWWAG)mNg7X_BME=&UYoq>nc0gtk_YoXNb5hYb!hG ztf(P(6Bcy6`wroiv-5NLLjVBx&|;W6WwKMmB+ph%7$AJfV95||OktlFlTMqdKP0i#Y*rj`(XeYUz=adk`3hA(LvO`y z|0%R3GMWC#x}RbCNX_Cf;_wEOS}%lqj#-CXQDIpi8Qis%Radz>q0vjbY&8DdR>jXU zmvR%au!=9lMN?P=hzQpNGOJRw?Cn8@B@kEp4r5$bgdM0?Fdua~*H~mGTf}17rZog% z!Kj#>m=l>Po$A`_fcT-pHy*aya+n%rXmG0CJ6a{nF%>TfyzKC2Dit7a;!8r;X^G$~ zS03MClV}lI)S^Py2I2rLnpjR64L!#Fl!mCP0td}~3GFB3?F31>5JCwIC zC~8VAun2Z}@%MZ{PlIWpU@CJ06F_<61le-_Ws+FSmJ@j>XyyV(BH@K!JRR^~iGjAh zQ+NnRD1C)ttcyijf*{xky2tyhTpJvac8m%=FR-LL@s>rN`?kMDGf2yMliwkYj= zwEEJ0wlFp%TmE6|fiti_^wVrxJ#gh7z@f0+P!kS>c>;BHH)N`PW0JHTqA?B~fz6H+ zdQq>iwU2Kne+4kR2e~l2`>(-^qqujX*@|w7k>s=e)Y-lwoI{$Tx_2}&y$9LZzKG-w z{TH06d?a9;01ze%EvqDCEt;qAaOYdf@X)zT)ScQs**7gQ**A5+o9p#P*X5~lMpNl2 z6p=Ecy7#f++P2sk;I2Nd`w-!5Y^3QHV0RVy2<55pqQ z&Q&b+JIKTf&6N(UjwrECT(BwKhkdpc#(Aq= zyG*N2frC~4B2Ko7O)bOHP8(}XKc;_(GP&+{?#dJ;Y$YXT$y<%YZmc>C?Sik?i?6E1 zk~VKGMLlNws0d#wk-11tBrAf?Tbes4F)oqxr_*7R-?Yn4IlyyP_ce6(J&tXSFI~P^ zYG1K1&Y@OY%nE}Gsa8~iq!!=l4a+yi7?Rxi#owl|2CnVfey<;AkI<2^CN^r`;-)ob zX7Ccao0G6Ic0ENcm7#3(8Y>}hb9aL6Gi?llW(Kss_CW07Z*0rgVhbod7+2-z3EC%( zq7QLJy|>bn^fyDVwISg;I%*4-lpnL5wLoe=B5sV^!Vdseg%7piW`#>KU*HD}MZ&J=jCFG;)9zqX;~A15Xsg;+mAtJruykiiD4Qc5$;lWT@^-j>F$$|0*{U zmrM6Kwy7I0>uJ&DC#8>dW7&)!1!_uGQ@Mvr)n^bH?_w|*J_E0?B{C&x%7+%$9&Umb zMv=?f8jwV=X`(6MfQLkyXGt_A~#T^(h~B7+v?~%F6k&ziM^m_Cqb!a zf0y+(L*8N@-&FfWsxPx%V97(F{QW`L&>2NJyB_}HBTWa|xRs*TT-y}_qovhF=%OCJ zf)sDf8#yYtG3ySQ*(qqz9dXI;CfS6yLi>4H9w9ii-!j5NwHL>oEN83>IsEP+V_1~u z`?}q?(o8RjDY5V?z9HC@t*0V_hFqA|HyZ8k)T!UJQ`KEKMLlNlIq<$2s!x;)o#SW0?w*zVYU?yc(v(2qyZg z0(^T!7Qzhpm)`?PLS7z|(>s+ZUO?_>f0y8LjB9{7he}@4-%l99L!vhyLW=yQr!);4vCSd-wC1QX-%H=?#UM-D_Wg8t3W z0*rY0Q4xwb5i(lBSOs^u(IgRSP$j!PkhbcIr^rh}e})V_kU5jW{q)m0CALP$`wKi& z?444cDxl;D;SqSw0^h%eA6Ro@BhxmD!}qpGb6OxRi6;iFai!)ctW|gmF3jQz2*O}Z z*TPvZAxFr1-Dd!53U_WQMQh$aauyVf;O60e>&G;Mg83(TOZt!6;s2KT{}By>k&-_m zA1YA0q3ID6fx`!qxy=@dYO@Rn%rEb~7P_%;Dxvl(WAfiJUtti0?~ah#_1`K#A}P2n z7^D~GQL#`hC}2w`btD`i%)VBWnn*jWF=d!kI*6T5-wBdsT)$EZD=mrn&EhxJQ^3>1 zbLeDA3&BIDAv=kWsp0t6>a3lITA;khMX^(B8Ecb^U%P-|RNGB@XLq*Q5a zR9aZ8RFNDYvD`dcva-5ti*`CcV%ltLG;emYG)5Hvo^Boe6!Fu0ekZ(k<<5G3_4>Mg z-?ILGT9yB`Gy?Cnu(PO#(bsKyf9>@F_MJQFZFaBE?dA7x40K@HNwA20g&JE&q z6&$MUcmsL)Sq;;@a9!*!?ct(XynVCJutm{pZ5w3Xci1lQ!9oB`xCdL! z6i6sX5X8iljX<8L4KC)P_hyjfBo3W=8BfQ5^inG|_NhXI*k)fvrDRq;Mtl#IdM%t^ zo(9yQnnQj}I{C__YBGYykMvG(5)bL%7>X@vm&+vnDMvZ(QMVC;#;@DZ9#6!r74JA`7phVA#`JE` z>BU^K@B>jj8Maz2m^>t$!%J^m)e|Ylem4L>e=OHtOVBCDy{0or$Np^VjdNl=g3xT8 zqsE*&O{Q9{>LhP;F2vpR<1t@fO4^Fbd{cO753U@l zLFAlS*(cze1w03?ZyLxG9S&n_udo?=8ddzgt#cv5fKd+uyogyl;44IK1&z^wj=!YK zzUD&kgK%`pt9A4nks?WMImECKCAt*xUXcPbo9e1&PmWU$X9~!}HO|j@r(`+=V^^Lc zcLMKF*Yj`EaS|pmb1uaDbkZvx6m%4{=z+MdgTuv?mT=4T&n?h7T_tQNFYhz$`~(DF zx4T%9nS-@(gWPm3?tZwJIpHDGWzAJ__zZKP;Hw>~%&n=s$Pn?6CaJ>bJzY?o)(O#~ z1fxWpkgP7ukZGyitR1C364Jp*?#{WzBom;9o=XrY;V#_Y5@5*}T5v*hcW#I;Sb)H; z6^g4&{fOcGP0zWCURc5J$ExdSY5s?r-^r#;|BS)8NjQH2--6b}!Q-Aa$mx_pNnz4q z(1_zCdqOu|4b4oo+-*jjTTV_j3WmL9=u`0(l@>00B5Vg?4f?fqwWRCX*2JwC(Yd+i z5A-Rm0r4e~4ceSJnEmWF6Nk>Q;(7sYyQ<-CgPa1fO8m6_pu=Maf0e2hd92Q#i7j?U z-VR;%F~r=@Xs>J2`Nx))UK=X`Shhg3AWzbwE<#%hM+KSQ)y~F!~7j*2}qu zgT9Z6kE4Z|n9Leb=N0%JnFI$AeNrV+!>E(WT7dyOjN~44BhNVL4(%Eo(1JGjS^)Oc zjSPsu`3wT8k`$>Na;G3pMU(9;+ov}PpiRt6*)WNMy(rEUak-14^(K`73yJ1#LZna? zS)ypsH=xt_ z1V%Pk;E@JqJeE1&xI}|JylZJSsu+mw#r=)G*5DBGv*`Q|1AC+!MW979QEZ{H5*8ZW z_U8EI1(M1LDjG^#yy~(OGH)?SdmR~=ma_^2Q#k>)`v#$t=~Ih|79!ZutXQTK^S&w` z1)ONotPDL(cz!_@bFBBOo6W@;7Zz--d9JaOs{)ss4P|Mr%>FaiMR=(fn-Y3SA->6~ zp`5h}dOcY_YfweZB*^el7qqa$&_r-Lg-I+9~U z`JxVCD<$VmoiR$g^3dU%7Sij)XYi*?$#ihSxCBHGOaRRr|Lo9+E}O~M>I}tnokI`}F32Aty#b8rpABEKl|B;*o8ge^^)Kyk z0!(>gFV=c)Q2Y%>gz+sa3xYTUy_X`rK5ca{{erC9WJ3EPKG{|Nng_-78kAD{oh_=K zn*wopK3cG}MBJf%6=}9YouD;zyWbjRt%A#pWc1zb3@FB`_Q~~UI!uvse(FQfl zUt=Qy2DSjwpzAUJ048~^;@Yo{C56R_8nZEeF}vm)0xoYe0y|tYI!>Y(d}mSro0`z; zeb6Eg*(a2{5Ypj8S$-_~L)+IlozZn|Iak`$jQKd63hldhts0=m>k~HC&`@|~;XaG6 zLVxC))8>^?13P*mV#ydlkC0V6AWK(BjWpqu| zbh7#bkKuL<kv5;Emm4zkF;X>rfbzAc7!Z)i};f=*bypYUD zho5-B5n;)FP(nzq8FG3TH?7l0vS{G}G9@~zxY>CqbX^mb$|JncS3I_2RD@?I9bz>LbX13A0N_LQmd(!3AxqmR_;3bJavc81%v z)Q~pDm0d1VrVe~>X?GOUOz94e6Nbt|fe6(S@cN64Gy6{i*TPukTmfvgPR>+qe>)@w z8mS6=rvR0~cqVfEWFsL|kZ3t~m-iV}va(IjJ;Hh4R9uISa6;@9d{D+7CwskGx!7MGZ6|rdE_I{cMD}-` zoi0%doDSznN-Evavf!_d@UNJt*Fl;hNrnVT2Fal8iBh(LU^l>8I1%x!q=6A@zO6O} zs0R@~z(6E;t~6L7tclb6A}zwwIvS;W`?F>>P)INWt6N9r4JbH*;&^6B!lHNAY+v3R zwCVoTTSL`1XtRZ_9vWH*(HcV?PImcNBOtbC4{U(v-HA~xMdpP8<);Xv0y_e1i%t|f zdyL`MtgjoC^Z-wGt@&6(9Wx>;qYcYwopK7H4iejT?T|>BSm)-fV&7yB;ANW4ZRzzc z?^;uh#-bDq@QjjBiIf-00TSw~)V;r?BHNEpDb(dLsJ_Z!zT7<{oC-V^NTEs|MeD0- zzuH~jmz>@&JaYIW>X&?~S>~+R!;wQOq|+{tI&#vV^n%|7ksh!vXzONlSb4zc!X;}> zMaUjix==sr4oMiHxL@~MPL%PrMzU{DPuz`9zWln9XnqKqNo3TZc;22OZ{ zy(90FLmd!qHIv!b-q){c(0@VYnzE(k5#rf~N5m{u-X za_J$`vM`7Bh@_`N%&n~35!O^m^pyWGR65?W@EH_fG}veT4I>@L72iny$1yuwBopv> zsSxe4Htw2+2f`M-+7|iva$OjEp*e=6r{J`{W_IyMTo#x0Yayp+V8z~17Hx&~6G%t? zN=#7bc$BWFl&qzMvU^iRl>Rvj(_`fR9T%ZBYX1?fg((%9FgbGrBl_7^rRQW9GA*@E zLN~c4F@W|oNmH$kHZ)4U$u(P4S;GSPDy671d;6L8z}?RfSb0PHN)PsKViOm_PLB-7 z+-+jjpC&oGWj(BQ{|L#DFOC3+-%fvGOOx^u^Ysxsq)Ox4^;}rM$!;(?`m@wtkXb~%u$Zx% za#IBD9hq=no-2H90jB}1^>TfWp)=Sb1v9w#UAHvYbn1PpHFbB+hwSXWK(ta=^8VN< z^j!PhT^ZXf#;?$ZWkn?(vJ20u-_SsGO1os)z;s=hI)d6iN-4mC9>EtcU@Mybflo@| z82lRHB)FEu4k@P9W+a)>t{^Jl;)gL&tWZBy(gWmfXX8XiUdnU>LtbceRd2RogiprV zK3KHRpSd5n#Hy5wQ!-Fg;{(9?K%pRuAEZwPR-E)JGeljq?MUmP=K$zkEO46*td&DL z%C4c|+^C204zq3rsTdE?%Y;lc1vKitClZ79P)GU-k`VCL5(kX_>5D{)C18r$^duj) zab$~pZ#$FLi^ihhytr80x6p2DsA3IsHPguaQ&s4izcL;7qGj1rPQM)4uc!I=d^j7S zs{`eqUlX0}s<8@_Iij-NBLD<2BE3VJ&k4Z6H;z?!7!7-XeeC-aX{Tl6ml!93m*cFJ z#Z5Q7fr}UC|2wXN*{|KEWPZ(V^*agnsVlrYkAd651IAl&yHxt9OnMCJBht5xn*lR2&NabYN zSWC^|d16K9!d@LjLiX4uEhz;%>2G#@i;bdI;t=8bK>y@P)WT!mDr~z}pG- zRg0M$Qpz0mbKF!xENTw8!Wwu{`9|04Gou}nTQ_L@`rl58B6UT^4~-?*}V`fYfKSaDIH zavlsK6XsL9-WmdH$C72oMpwJp)?;)Z4K6Es0B$SXP*QhM!gvpdUyI?}p1c2yYhY~r z_VvRqI~hi$_97U@cE5#Z{Zhy&EqB*`vAMpf?Ya?h{;uuk-}E1T!ah4kx_Q*9mOjl* zv62c1x-eMCSfQ*b3b|P6*~#_2>fN2y=iJQy-I$q_TIV>AHLGvxzY#v#{w}OBR>mny zZ+4AXVq%F7d*h&{U!c8&&KUXS@X->Bu@pTF71|eeQVYw8ns~h`7|n?)2@d35c_1Jn zeG)5*kFZ<}MejgYN(?7Nw?Mod)k5v*wm{$@osr)Ywv-QvXpeI;3Qku^T}zo`go?co z|65!$tORilITCe4GfhNoqaj~NtO|@obiA%Tub@&qQ)*Sn14oz#=<2osGcxe*+@PL< zyx=_nR&*Un8g$Iu#el1FV8xS6kKlqt6Q_nLmsoyCCicctlpM=xVMApO3V7u00mxNJ zn8H5H7~1cY0)_}KJSfc2QSG+HDoQlkX^Iwi_%Qb4&1XPlDw$%cwf-dlhzTK+<_D-) z&P@=34aLr)@%x%0WcLNFBZ4im4biAYc zX48#WytT#YP@@jEfGgaR&J#HZzJa@HjxyMYHe{pLPnxkn;~Nj*Rk*wS5*frI0o^@# z&G3U*-hF=Y_v1Euf&ZeY$+hsoi~%M`iq}OU5nnKjI6qCo7#tk{_f3pIO(8(pMmgCr#+;(8d(-5n@oY{gBKSFB;sfY zEGd8%M6}wgw88w$*dURSw+YzI2N!gycd}~V$*T@AlPt*-f=web80-YsRGL; zIurEoITNgt(oy6p0G%)TAq})jmI~qDOTd#8SWUAuE(*k}kk&NIGfR#?MWZ&@WgOiL z>$#C7>im5ft}NgVUz#o-;GS~3h`u>vuPTQ6J_?slXE&+uSm7V8X2xqGN*g32wQVF? z60uDVd}|BtzXW}IHl+O9$Y${gL@oN<={bc5POfF*UaM4*ulAX=jeCFG9716kCF{ap z+Aa!D*;gIV6MjhUJ)8P&!?O}G@h+kF9lXMn@bE1hm7VR%NpI0p(h7q@gb zs40V7?1#wanDpa((WWtV447#&s#OHJWeK>i<+;H67mI#8cP#nvB-$#8&oY@Q_cX1> z#729EG?sBvSe1t$UC3o?5BSvkVN@w(QQ4cW%3w&{E71?HvJrUEs@C5uiGi2-#9RzC zw0R)RSq1PMNN=!DdusVZwDksjyaAQbNru6UwUWxld@ldSWo?0&)`;Xs$LTI|<=N_s z*4BCzi%Pnt37TSLENizfSMFGy!FQt!OTgaGufi;Y{r$=cJS)FXBg|11{Y)6 z&FoDw-n6}+505Cb=XILmcU3v0TbML}3&IJnbKY?t6@!3@-XG)E17_uq1tu zz$~wy7yG89CHH-vtG}q6Z~ttOmW){@%R~RrHPL3}aSux$jl5%aPq}sjvD-AQns@b7 zY@Oc;tRc(`c(&eQsK@oDdmBD-*rPabNn z(VZVY5nz7{q0q`4KJLomsMOu|s7*#%-xXTM-Iq0IbER!m(6>i7*+fAfS`~--GwXqM z4ca)XqKhhrI<(1CRvrYaF?C+w%ux-FklJA!x)gsK+>>%M>?Cm`XxbwUj;EAE@Q-G= z5cFv(Qwcw7h#q)bu5EK58r1nZ6^FodqAYE;KnPkOE*EDluO!khZFyZZGn4S2qu$k&M8jDj8T_CbL0QU?r8R{_G)Wt1$pHq>0cP3sbJb9fA#aCxY+I-RDFonr20^=HoUCZRYU z3;Wx@Q{b+BZ2dl{1zxcqS5d}TP9^VEZo``(0%P+4>^Ho?uXD2Rd}SjDvjSCkh2VrA zKWEMFMooUWGVS_sQoH(GX9QMhVu*UMH=Y!B(2b48^*fnH@gfxbGf<8rF%}3qZBgv? zh(JU+*63i>>V+rSOX()d6M}awEy>N7L-;9D0cY+eL%cJ})#Owz>4SDuWjsapJukYm z#U|itkDzOryOj(#d47LERC;) zr?00mlOxu-u}_c>)3d=1nWQ1_>F0k02%Z<)U=_eaKsaOFH4zrLYa*;@;Akf7-~g~P z1n-xT%i0(jSUv$dfNPE!IynMu{+t&lDe21Kfn)7m%JJ%C)HSiGPUMys&0o#k$Pl1AFx2#-J9Qk{BW?yJ&d`)AH4#W6I1ps&M36?pz z;*EEoPlL}Wyd}~t&>61YcyLUW`L*Z@r$ihqOO<>>P87W7%w)RnriPH5#PubXD(#Qt zb=`}6I@RDHQpY=kNa_A{ANlk2h1!-L-XsS9{Yde^7JZx&lBt*$XJa_U*{MPcyegB@ zLiCqy>-sZ1zHFGjnK%FwzcjhG6;2~wQj-;X$(393Gf(VA30y8mnsPt6v5LGPJu3eu zY%}lS@YZ2aSN!T?5YGnE75@r$2_iPZ7L`-9i-c%-06Byv)+f~T;|Gd|m55Y+$g%Bm zPj}UPswtB5NxC%9CW$b6C5-v-S_M4W{9XsSP#qo;3y`eTAPWR3Kpk!&Td%m;xeD(J zkgb$2pVc5gT>4^o<`c@;15!fPdzkh}4{kYM1SD4KDK~XdJLN?dXcN3q2h=!JPqqSs`ZYWO$j+JfDLj)AlVFaGoLZ`FsNhYa`KNgLG*%}AYs=;H z-Q%gTlisM@(w$LOiPoC~Zg644D-NihWG4QGg)6mba_C<| z;@RIbtg|gW6G~C0*G;5-D_|-`wZ2&m1fZD<%P|7sCJmNjGcn=gW2)16WU#O`laDax zK8Ni+Aoi>@VK=3s;#}xhR^9Jzw%MFc&x8*v?<7KQc~eC$6!C7}T1I4g>`)FZ;6Rnwc-Ku+?+S~*U6eo2GC z#py)*DBdbx(@JH~ypn7wmCD#+D?O9fB53UEWb`Rx5qG*P9;QEqBx0pe!g%R;g<1|W zMu{%gG1KRqtpu76i)yF|p#XiLn}Zmhwi8>MGujfX&N?{@xCESOraYg32W<;>eAK%n z={*s@RQHJgpeK#FTvnKc6_gCq#JuoUie}W< zt!_}JcJdvs(L`=w;$Bzoa@0VGU*b&#h-6ubG#6sWaT z*4e@S?>9bJF?xvi88VQ^@r zKb^NY2to+SU}2lC7kk*#5^CKI%J*psqC;BRr_+8)Xi7@g5@;Nvy3eEf#ln6AX4h~MMTk5c4t}yc06aIsgVKpin*eIuxsE?F&)z#b;yzjfuy#dfqX{bNPrN@_B>{_9E zTA9)oOozvwO4b|3^;LmSq(^Y$uRpK4e~~g3$WV`$-BNHg_JV8Bv@!_>w9>pL(8W8T zSG4bRrDxA@u=P5Iq+vU_@wG*u!cg_2hU(^|WjF(DGEeyX?=kLU(a;!+whGaG=fSNk z*d?J`ge}AuLkq8o<>B87rYJ=#c@W4vb7cAbZL+a|P3JNNTkMid`+4ty!bj+3z=Hu0 z2k~HtdJ9WD2XZ{)`#7phzt{sp23-LLii+4_=Z+?tI+p-T*MNe$odqR$OZ^4Ug5CuT z>i1p^xbmEkI^S@5AhehRFD01*!L@ABtj*r?4~-95ub}R0(7Iwut*5`#qILDD6W_+Y z7)hdJCyOScg7TgL3J2FgP@G{DM3nY%3J5%E4=gG53uob>YW;S3YOCMKEWp2y_pULd z=p=qD$*^aBEj`$6MpY$1=Rss08VHvfrz0aIPuO$uvA14Y@(@0v%R)ODP2>dYu%KdV z3le_(DM~MIPhf?ZG*^A{jL?E72-d;zxY6Q_sWG>^d_+41@mMh)5P!H8)>l(`oU75yjMi=)QZ5O0~QIy0S`KRD5!4!wV>5V?kFP{XPF5va? z8WGZv+8|*>b6RX+2UjA5NFOwz5p0Xk%wVPkH~B_fO|%-3SAXru`l;Bvj)VC1llyI#qf&7Wa-Y(RzE&hY z#c`VnHONe7V=Y8iCAFyTYmIZ+o7?S*PF%lCmTuSQ%Jo#!vaWf%RI1FfrKD#hkY^wk z>Ol?BIebHZxO^o#6XIxE5=%gk`%B3fsR3KJd{z1=UolnL zxVJG*lrB{j4QrEo1?2fkWeE@8QtFVo#bYKD-BTwXlsAn+NIb#ykk;2~i}Z^tL*(2) zDEj^l>+ymTQdwjrNTKb<0x2!h66mc&hT9y_TjZ^<6q!w3JlFH^F9%r}bVg%n`#$SA z&?V##X#;j9KdvHYJ;nlu*FKt&fVUnaw~l6VR7w7Mh6<%OUk2tF0U`-YdRCIEo2*N0JceWvAO{% z05P^$9S&j+i1P&7jd02s11a{qeAFhKXYn|Z#^q<%L~&7E#{x}TCh%f9zL9B;_`cnq%wnr{i$aybv{USMj{H&n;e zC~91brnUfLfZ$-d$uYF~3IP{V_iN_BMk)+?D8L>gm}S$!?t& zQlV)1kc4Sz^kx9=TMR`7EF>s4=Y{5@Phqsy>A;-)7co^s1!;p=U*}pMhm{+p@Vufq zatXMEDqvV#Y82v96zT<7!oqk$@r_WmroUiUA0ETO)P?^L+pKL?*#5@C#oGCq1U=5Q zA0g$CZ~r`Dhx2h-IFJTaeCVSSfwE;Ai~U4%Mq7m$8A^hr2vx1wxKsjlVJ*taD2inZ zTzJ!$3*)*Mowg_q)qb6JF*!R=E}uk`Izeuu4*gX`kp(D<1DCh^tm&)Ddt~J}Qxsnjwv(tX8 zvyX!L<$1uTZ4B=@8GX|K7p-NHRI&kObG=6SV0YmbkOV-TRnI zO|*+T>1{%)>Y&?HHZ}6B)M-B$(%6o>e)DT`N>B^fzZz(E#-_Zl+AUBz!y!nVaDOy2 z$3u6pg1+`qnWld>CufRs*74%yV;3YT)s1-)(cMSoXga~Vsd(BP^rPAa)$jC(-*v@% z37zH!198UphLe}-S3Rsm`BEDOKWWc0w{xqA*NctylQ_1U7V-~4#VrQ*?E^Rv8KvWdt1NJtqcSn{#j*j6w z_1fbstu}x`G<;}0Qkh1vRW!SfaI804LpSoumU$ORzJWX)cqNKhju>)fk(kqM3Ml&A z!2Gp=M0KTb2SOfg6AZ!n)LNnKv9DJsEvO069M7@{505>ElahKg5amp<}T8K&fK;h(?6 zD8mw1UY2+wk3w(U>HbZF1W!;bJwh(oaCX7syZ3Sf5xDMzI?8(|Toe&WF(R&fcQ+c3yu={`!G8FXR6UiyIUh!wW8&E1JhsV_F+0ryRogcJ z=mjDX`rf1N0|SyXNpzx^Ga$E{xZ0rjA#wUl`H)|yF6#O1-j|5DzIW3t#yt+7 zcNg7}SUGs7>rG7>bWO7Kff`(5%~@f&g(PraPAi=D6r5Zft>_!#dM0X0J+$2_BNH?R zoa|$Frq!Oc@hvp^n3_f=wL8pkIYe%I^NNz0o<~a;t!-9IusL$bf5@y~j^P}uJSmA`P$b6?hqshH+!(Lfw%ZzV&R@ zSeM4K%Zh$TpIJvl3*Y+435$*J^=n5yy{_hfE7>NG#EjgVvP#5-e(CKh=sppX^maAE zNX<@{IQl-T&J*XUGd?M*u+U5u(r+=mRT<)1Vz2x=5(;T>kq3-Km|}E3Yx(Hz7#Fh- zz1n~3Ra5b{ZofBz<>0=~(tV~a7j=@I={B{}SvEEpZ~--V8|+jXB-+>wb+%*PSrdZd z7M{LZGk~yc&-P~2ym$d(y&q9q~N)W7GI1>>$$4YC(l9;BI13c~kj3e=Ud&dSCF}&uf?M zQd!GHyq=ro4Wh7xiYat>cl(8HtY7Wh&9m~CO^d~rM$q3WUk>W0gg4=VV7}+B=s|xE zyE2=a+GER^wZ<-ONb~odKoM*{ON^<6vCMC38HjZPl4594l@+cg4VO?`I&Mo&us#aV z&!-u6$QGLAU*#cd%#fN1kMNt$1mqiRebD;4A5quK z7G|4$JX+^DnL|IBlVhRQcziEzlnlzG*w-%kD?5Go)@k3XN?84TAp`fR>uYF~{~Kf29!G+~dPVdddEX}m_7oomyD(yDIatk7$|^h&!doNXehDBkck zGHZHZw^gsxnR%8Mcd6cQ*_(*8?TI!o8~%Cr!~0;J=2knihLxO6xsTalBrM@Q^UNyj zVZwsht9y$YVubn_ZZF&fuy~>$Y6f9uA@PKi>23z+Q7{K@vT87eZ_m5Z9YJQD%FARh zv|zV|_NH?_O}CC$;*4S~@fX=kPp}X**M^)lUdx}$t*&sF_aybYoUtxbJ6e@BL}bl1 z!gT6u4CD@44+*4-XGo_UwnuSDFq<3Yni%th`w)asPuN!fv`@Vk1Q{p(l+*v!dyUnU z@o%Of@J0AD0uM(%Sh-G71j(L& z#P>w2frh%`Q@B-Vy)lew@)RRbW1*xiX#VUh!RrokQKezDMl(Pi7&LpTQ4WmY{j%mR z>8x+w^%Q|N=rgn$>1|JlTu_p;q~`Q0G8B^T$>eeq+Te)oVD#ZgMAFQ$_)mrzjB|g` zYS5--U%iJr+>7rW=v1SQV+cxz6!kgQ!XCkoVvHC1QeKbF9MWkg!Dv_QAffz)dg8!k zQuE^sz}g^`R)c``sZ6UDkCt|Y0SPUFV}87$sgh-)j|KOnk>d17D!hRm^A=XVt5jh> zMLY7^-f@~ojO8e$4?w2mp$dkaKo?OHsn3i~zb0SkIrsVb$m2nO#Xx9kGwk)6!4yOg z?W?Bf8f3#FIu_n8C|AH{1iDH6^kk#6ZboKqIJf=jSvq;s`D^5j0A?78kZwAX1j!|? z(Ro#^<*qj68no=MqN`!UyC{&DG>|2Urxzf2d<_NMv`I8MT!f0TR}vyyIanCmY~t>P zuspc1JS|BN^x{Pmr{`zp?V)1mH{!WDQe>FU)D^N4h_)qgYCDy(NQI`tsiKN* z^<&J-v3;7VsAjVwtwbGO<*WB+#)?m0!8ba$B{?vfrtw>+A=x918Gc4%Rzxucj&tQS!w@i}(J^sJ zKFQ=gIFhUdz7R;=5Xpcxr~b0W)oYr+jId!P$MPYlSqn4GDWT{fvr(V(8v(p~mc2vF$K-#w&EfsA&V3V^Wqp-ulGl!{yL& z*6TF`2H;Ub8CW7d@LsE;%sohS2y_ToSXhW%SYPqNs&~`YVE;h_*ne>CCHR$Y^xYq} z`k!q?Y-}9CTk!_A*Ac49jt2IQ|2xup8^BHXJ?B^ONKpX~Fu`BA4}xL;7T~&H2^(HR z7&+d^l?!%KID`Ac-+?`)t!-Zg4^(p`2neZPz*xZRrGEwXZxT`6mhqYRh@di9xu#$_ zf0Z!|>@>d<_J(Z2_NGo&;M_i9u0{acpH7(DVB_Q{?2=%xI`Arx^A{QAkpDf{KPa-E z>5xbYY@f%75D?cHjepWP_`&pVCAygu@wOOpFpM@Iz-%9YMY-NQ_(_@Ikdc3j@S}bf zIrEQ2>}?Dx#Y-9;u$uD0&*5LYLnHQYV+fmoyPY`D-oa7X$?#9J{WUBq$T_qO+!a{C zU0(R7T;QuW`2P*|haw&R8qQ9&^BFd{(}#mQz4R||W#B0E-_)cCz{JKL@UO(w4)}~-B+Zuo!lK*p3+_vwbLeSM9 zcxy@@0|Mf@B<)XPqWbL?$lOuy@HX&zPIW>NSoCf%_^&E=1;_UPrpo1j4h~>pf7lrO z5CA_;9RYuB>T>q|-DWWEG8p$)fs?_x)_xQBPe2y~d%%xjbO-RwTI*sz)eOFx1i#V$ z6YxJ7_h!-V>mu$yiH7?>LjI$eH>)52I&zhH|0Cv)p8VJ5yjeWw7Fg;&-9{+J-k1 z3jc}_r}+;Ee<<$%uLN*ghMP%NuM-phq-O@di*VN)`DQ*($)6zLs{-SH!uj_JTyINv zGm|9PBsVD6m-#wDbwr@(7#Ptd0VKP$@Z?ZKK`T%;BWE2 zE#lwhfV|y+n;CnqbNc-xb<5vrz+djm-u0AN@MNdN!< literal 0 HcmV?d00001 diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..eb9194764 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/mvnw b/samples/rest-notes-spring-data-rest/mvnw new file mode 100755 index 000000000..fc7efd17d --- /dev/null +++ b/samples/rest-notes-spring-data-rest/mvnw @@ -0,0 +1,234 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS + diff --git a/samples/rest-notes-spring-data-rest/mvnw.cmd b/samples/rest-notes-spring-data-rest/mvnw.cmd new file mode 100644 index 000000000..001048081 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc index 420681011..df16161ad 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc @@ -35,7 +35,7 @@ Once the clone is complete, you're ready to get the service up and running: [source,bash] ---- $ cd samples/rest-notes-spring-data-rest -$ mvn clean package +$ ./mvnw clean package $ java -jar target/*.jar ---- From f07f23ec85e7654346087b11cdd0f8268c791d5e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:49:07 +0100 Subject: [PATCH 0173/1059] Update versions of various dependencies --- build.gradle | 10 +++++----- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index ac4c5afa3..35db2d4e7 100644 --- a/build.gradle +++ b/build.gradle @@ -56,18 +56,18 @@ subprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" } dependencies { - dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' - dependency 'com.samskivert:jmustache:1.10' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.samskivert:jmustache:1.11' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' dependency 'junit:junit:4.12' dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-library:1.3' - dependency 'org.hibernate:hibernate-validator:5.2.1.Final' + dependency 'org.hibernate:hibernate-validator:5.2.2.Final' dependency 'org.mockito:mockito-core:1.10.19' - dependency 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE' - dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644' + dependency 'org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE' + dependency 'org.jacoco:org.jacoco.agent:0.7.5.201505241946' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index ad8688f23..020030dc4 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.5.RELEASE + 1.2.6.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index f6c8ca4a3..22a2e03e7 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE' } } @@ -36,7 +36,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-hateoas' runtime 'com.h2database:h2' - runtime 'org.atteo:evo-inflector:1.2' + runtime 'org.atteo:evo-inflector:1.2.1' testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' From 9a362abe7796207eef9129092cf239500c85ab94 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:00:33 +0100 Subject: [PATCH 0174/1059] Update reference to mvnw.bat when building samples on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7538a6fef..e05478d6d 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "mvnw.bat" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? ".\\mvnw.bat" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 75e965c7823cdf5da9db4c1997342a63a10d1772 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:15:47 +0100 Subject: [PATCH 0175/1059] Correct the name of the mvnw script on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index e05478d6d..ea9672201 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? ".\\mvnw.bat" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "mvnw.cmd" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 6443ed177e22a59084a016b1e5809abdf916c3a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:46:27 +0100 Subject: [PATCH 0176/1059] Fix building Maven sample on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index ea9672201..db94081e1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "mvnw.cmd" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "${new File(this.workingDir).absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 0213c9e6fdb2ea418d8ea05b8b8eb866cc751408 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 09:47:21 +0100 Subject: [PATCH 0177/1059] Add the Spring IO plugin to the build --- build.gradle | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.gradle b/build.gradle index 35db2d4e7..5fa1b286f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { dependencies { classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' + classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' } } @@ -51,6 +52,22 @@ subprojects { sourceCompatibility = 1.7 targetCompatibility = 1.7 + if (project.hasProperty('platformVersion')) { + apply plugin: 'spring-io' + + repositories { + maven { url "https://repo.spring.io/libs-snapshot" } + } + + dependencyManagement { + springIoTestRuntime { + imports { + mavenBom "io.spring.platform:platform-bom:${platformVersion}" + } + } + } + } + dependencyManagement { imports { mavenBom "org.springframework:spring-framework-bom:$springVersion" From 6debc593e567d74ca1d6ed9bc3ea90b102a614d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 10:38:50 +0100 Subject: [PATCH 0178/1059] Don't apply to Spring IO Plugin to the docs project --- build.gradle | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 5fa1b286f..593cde259 100644 --- a/build.gradle +++ b/build.gradle @@ -52,22 +52,6 @@ subprojects { sourceCompatibility = 1.7 targetCompatibility = 1.7 - if (project.hasProperty('platformVersion')) { - apply plugin: 'spring-io' - - repositories { - maven { url "https://repo.spring.io/libs-snapshot" } - } - - dependencyManagement { - springIoTestRuntime { - imports { - mavenBom "io.spring.platform:platform-bom:${platformVersion}" - } - } - } - } - dependencyManagement { imports { mavenBom "org.springframework:spring-framework-bom:$springVersion" @@ -110,6 +94,22 @@ configure(subprojects - project(":docs")) { subproject -> apply plugin: 'checkstyle' apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + if (project.hasProperty('platformVersion')) { + apply plugin: 'spring-io' + + repositories { + maven { url "https://repo.spring.io/libs-snapshot" } + } + + dependencyManagement { + springIoTestRuntime { + imports { + mavenBom "io.spring.platform:platform-bom:${platformVersion}" + } + } + } + } + checkstyle { configFile = rootProject.file('config/checkstyle/checkstyle.xml') configProperties = [ 'checkstyle.config.dir' : rootProject.file('config/checkstyle') ] From cfcc43c482d18846f8dc7c21d57a51105b315fbb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 11:48:09 +0100 Subject: [PATCH 0179/1059] Ensure that generated poms contain and --- spring-restdocs-core/build.gradle | 2 ++ spring-restdocs-mockmvc/build.gradle | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 2de27164d..2ba4b30bb 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs Core' + configurations { jarjar jmustache diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index 597741053..ba3ab171b 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs MockMvc' + dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') From 260ffeebbaf5ba9d70192335d8610b35c1c38d41 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 7 Oct 2015 05:48:20 -0700 Subject: [PATCH 0180/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 83e39c4af..59241404f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.0.BUILD-SNAPSHOT +version=1.0.1.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 69f6b40e28faf86adf5025a91b1f69b18c7a41d3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Oct 2015 09:41:52 +0100 Subject: [PATCH 0181/1059] Correct the links in the documentation to the samples on GitHub Closes gh-147 --- docs/src/docs/asciidoc/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 86c27c47f..17cde86ea 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -10,7 +10,7 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs :source: {github}/tree/{branch-or-tag} -:samples: {source}samples +:samples: {source}/samples :templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle From 697275f347693ff47e682ca2a4cabf8816b59cbc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Oct 2015 17:51:17 +0100 Subject: [PATCH 0182/1059] Begin development of 1.1.0 --- gradle.properties | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 59241404f..4797e2a66 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.1.BUILD-SNAPSHOT +version=1.1.0.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 020030dc4..274f9569b 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.0.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 22a2e03e7..414a9081b 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.0.BUILD-SNAPSHOT' + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } dependencies { From 9a3e4c5f701040a1eccf2786fa278caab860ab9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Oct 2015 10:03:57 +0100 Subject: [PATCH 0183/1059] Update the samples to build against 1.0.1 snapshots --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 020030dc4..aa6384b6c 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.0.BUILD-SNAPSHOT + 1.0.1.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 22a2e03e7..8dfb61e8a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.0.BUILD-SNAPSHOT' + springRestdocsVersion = '1.0.1.BUILD-SNAPSHOT' } dependencies { From 75085477b69fb5b936b6c9eefe80730fe913d30f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Oct 2015 09:42:26 +0100 Subject: [PATCH 0184/1059] Suppress unwanted output to System.err when pretty printing content Previously, when the XML pretty printer attempted to pretty print the content of a request or response it would output an error to System.err if the content was not valid XML. While, benign, this output was distracting. This commit updates the XML pretty printer to suppress its output to System.err by configuring it with an ErrorHandler and an ErrorListener that swallow any errors of which they are notified. Closes gh-153 --- .../PrettyPrintingContentModifier.java | 79 ++++++++- .../PrettyPrintingContentModifierTests.java | 8 + .../restdocs/test/OutputCapture.java | 154 ++++++++++++++++++ 3 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 49af2bf46..16c9471ac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -23,13 +23,23 @@ import java.util.Collections; import java.util.List; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.ErrorListener; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; import org.springframework.http.MediaType; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -47,12 +57,14 @@ public class PrettyPrintingContentModifier implements ContentModifier { @Override public byte[] modifyContent(byte[] originalContent, MediaType contentType) { - for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { - try { - return prettyPrinter.prettyPrint(originalContent).getBytes(); - } - catch (Exception ex) { - // Continue + if (originalContent.length > 0) { + for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { + try { + return prettyPrinter.prettyPrint(originalContent).getBytes(); + } + catch (Exception ex) { + // Continue + } } } return originalContent; @@ -74,10 +86,61 @@ public String prettyPrint(byte[] original) throws Exception { "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); StringWriter transformed = new StringWriter(); - transformer.transform(new StreamSource(new ByteArrayInputStream(original)), + transformer.setErrorListener(new SilentErrorListener()); + transformer.transform(createSaxSource(original), new StreamResult(transformed)); return transformed.toString(); } + + private SAXSource createSaxSource(byte[] original) + throws ParserConfigurationException, SAXException { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + SAXParser parser = parserFactory.newSAXParser(); + XMLReader xmlReader = parser.getXMLReader(); + xmlReader.setErrorHandler(new SilentErrorHandler()); + SAXSource xmlInput = new SAXSource(xmlReader, new InputSource( + new ByteArrayInputStream(original))); + return xmlInput; + } + + private static final class SilentErrorListener implements ErrorListener { + + @Override + public void warning(TransformerException exception) + throws TransformerException { + + } + + @Override + public void error(TransformerException exception) throws TransformerException { + + } + + @Override + public void fatalError(TransformerException exception) + throws TransformerException { + + } + + } + + private static final class SilentErrorHandler implements ErrorHandler { + + @Override + public void warning(SAXParseException exception) throws SAXException { + + } + + @Override + public void error(SAXParseException exception) throws SAXException { + + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + + } + } } private static final class JsonPrettyPrinter implements PrettyPrinter { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 85e5edf9a..a06c94006 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -16,9 +16,12 @@ package org.springframework.restdocs.operation.preprocess; +import org.junit.Rule; import org.junit.Test; +import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.isEmptyString; import static org.junit.Assert.assertThat; /** @@ -29,6 +32,9 @@ */ public class PrettyPrintingContentModifierTests { + @Rule + public OutputCapture outputCapture = new OutputCapture(); + @Test public void prettyPrintJson() throws Exception { assertThat(new PrettyPrintingContentModifier().modifyContent( @@ -56,7 +62,9 @@ public void empytContentIsHandledGracefully() throws Exception { @Test public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { String content = "abcdefg"; + this.outputCapture.expect(isEmptyString()); assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null), equalTo(content.getBytes())); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java new file mode 100644 index 000000000..4aab96685 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-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.test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertThat; + +/** + * JUnit {@code @Rule} to capture output from System.out and System.err. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +public class OutputCapture implements TestRule { + + private CaptureOutputStream captureOut; + + private CaptureOutputStream captureErr; + + private ByteArrayOutputStream capturedOutput; + + private List> matchers = new ArrayList>(); + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + captureOutput(); + try { + base.evaluate(); + } + finally { + try { + if (!OutputCapture.this.matchers.isEmpty()) { + assertThat(getOutputAsString(), + allOf(OutputCapture.this.matchers)); + } + } + finally { + releaseOutput(); + } + } + } + }; + } + + private void captureOutput() { + this.capturedOutput = new ByteArrayOutputStream(); + this.captureOut = new CaptureOutputStream(System.out, this.capturedOutput); + this.captureErr = new CaptureOutputStream(System.err, this.capturedOutput); + System.setOut(new PrintStream(this.captureOut)); + System.setErr(new PrintStream(this.captureErr)); + } + + private String getOutputAsString() { + flush(); + return this.capturedOutput.toString(); + } + + private void releaseOutput() { + System.setOut(this.captureOut.getOriginal()); + System.setErr(this.captureErr.getOriginal()); + this.capturedOutput = null; + } + + private void flush() { + try { + this.captureOut.flush(); + this.captureErr.flush(); + } + catch (IOException ex) { + // ignore + } + } + + /** + * Verify that the output is matched by the supplied {@code matcher}. Verification is + * performed after the test method has executed. + * + * @param matcher the matcher + */ + public final void expect(Matcher matcher) { + this.matchers.add(matcher); + } + + private static final class CaptureOutputStream extends OutputStream { + + private final PrintStream original; + + private final OutputStream copy; + + private CaptureOutputStream(PrintStream original, OutputStream copy) { + this.original = original; + this.copy = copy; + } + + @Override + public void write(int b) throws IOException { + this.copy.write(b); + this.original.write(b); + this.original.flush(); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.copy.write(b, off, len); + this.original.write(b, off, len); + } + + private PrintStream getOriginal() { + return this.original; + } + + @Override + public void flush() throws IOException { + this.copy.flush(); + this.original.flush(); + } + + } + +} From 58b992d99eaf277da7671e53c0a7fd3a65bd9534 Mon Sep 17 00:00:00 2001 From: cschaetzlein Date: Mon, 16 Nov 2015 16:15:04 +0100 Subject: [PATCH 0185/1059] Allow individual attributes in an XML element to be documented Documenting an XML attribute in the XML payload leads to a NullPointerException since no parent nodes exists for an XML attribute. Rather than always trying to remove a node from its parent, this commit changes the logic to apply special treament to nodes that are attributes and remove the attribute from its owning element instead. Closes gh-167 --- .../restdocs/payload/XmlContentHandler.java | 11 ++++++++++- .../payload/ResponseFieldsSnippetTests.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index a971f049c..89b0aecf2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -34,6 +34,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -115,7 +116,15 @@ public String getUndocumentedContent(List fieldDescriptors) { } for (int i = 0; i < matchingNodes.getLength(); i++) { Node node = matchingNodes.item(i); - node.getParentNode().removeChild(node); + + if (node.getNodeType() == Node.ATTRIBUTE_NODE) { + Attr attr = (Attr) node; + attr.getOwnerElement().removeAttributeNode(attr); + } + else { + node.getParentNode().removeChild(node); + } + } } if (payload.getChildNodes().getLength() > 0) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 337e6e8c6..720483012 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -192,6 +192,21 @@ public void undocumentedXmlResponseField() throws IOException { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } + + @Test + public void undocumentedXmlWithAttributeResponseField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("The following parts of the payload were not" + + " documented:\r\n\r\n 5\r\n\r\n")); + new ResponseFieldsSnippet(Arrays.asList(new FieldDescriptor("/a/b/@id").type("").description(""))) + .document(new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } @Test public void xmlResponseFieldWithNoType() throws IOException { From acb87b98b872caa5c591a6ac5e3930d93ff42229 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Nov 2015 17:41:42 +0000 Subject: [PATCH 0186/1059] Improve testing of XML attribute handling Closes gh-166 --- .../restdocs/payload/XmlContentHandler.java | 1 - .../payload/ResponseFieldsSnippetTests.java | 72 ++++++++++++++++--- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 89b0aecf2..798013736 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -116,7 +116,6 @@ public String getUndocumentedContent(List fieldDescriptors) { } for (int i = 0; i < matchingNodes.getLength(); i++) { Node node = matchingNodes.item(i); - if (node.getNodeType() == Node.ATTRIBUTE_NODE) { Attr attr = (Attr) node; attr.getOwnerElement().removeAttributeNode(attr); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 720483012..ad6653d38 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -192,22 +192,78 @@ public void undocumentedXmlResponseField() throws IOException { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - + + @Test + public void xmlAttribute() throws IOException { + this.snippet.expectResponseFields("xml-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( + "a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("xml-attribute", this.snippet + .getOutputDirectory()) + .response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test - public void undocumentedXmlWithAttributeResponseField() throws IOException { + public void missingXmlAttribute() throws IOException { this.thrown.expect(SnippetException.class); this.thrown - .expectMessage(equalTo("The following parts of the payload were not" - + " documented:\r\n\r\n 5\r\n\r\n")); - new ResponseFieldsSnippet(Arrays.asList(new FieldDescriptor("/a/b/@id").type("").description(""))) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", this.snippet + .getOutputDirectory()) .response() - .content("5") + .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } + @Test + public void missingOptionalXmlAttribute() throws IOException { + this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( + "a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c") + .optional())).document(new OperationBuilder( + "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) + .response().content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedAttributeDoesNotCauseFailure() throws IOException { + this.snippet.expectResponseFields("undocumented-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("a"))).document(new OperationBuilder("undocumented-attribute", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void documentedXmlAttributesAreRemoved() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo(String + .format("The following parts of the payload were not documented:" + + "%nbar%n"))); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", this.snippet.getOutputDirectory()) + .response().content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); From c559efb81b987d5ee24d6a8029312cea5201429e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 16:02:16 +0000 Subject: [PATCH 0187/1059] Provide examples for JUnit rule configuration with both Maven and Gradle Closes gh-162 --- docs/src/docs/asciidoc/getting-started.adoc | 28 +++++++++++++++---- .../com/example/ExampleApplicationTests.java | 5 ++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 136d98069..6a652de67 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -227,17 +227,33 @@ documentation snippets for request and resulting response. ==== Setting up Spring MVC test The first step in generating documentation snippets is to declare a `public` -`RestDocumentation` field that's annotated as a JUnit `@Rule` and to provide an `@Before` -method that creates a `MockMvc` instance: +`RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` +rule is configured with the output directory into which generated snippets should be +written. This output directory should match the snippets directory that you have +configured in your `build.gradle` or `pom.xml` file. + +For Maven (`pom.xml` that will typically be `target/generated-snippets`: [source,java,indent=0] ---- -include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +@Rule +public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); +---- + +And for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: + +[source,java,indent=0] ---- +@Rule +public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); +---- + +Next, provide an `@Before` method that creates a `MockMvc` instance: -The `RestDocumentation` rule is configured with the output directory into which -generated snippets should be written. This output directory should match the snippets -directory that you have configured in your `build.gradle` or `pom.xml` file. +[source,java,indent=0] +---- +include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +---- The `MockMvc` instance is configured using a `RestDocumentationMockMvcConfigurer`. An instance of this class can be obtained from the static `documentationConfiguration()` diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java index d94622f5f..70fb8e188 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -28,10 +28,9 @@ public class ExampleApplicationTests { - // tag::mock-mvc-setup[] @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); - + public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + // tag::mock-mvc-setup[] @Autowired private WebApplicationContext context; From 7a819b15b606dc60865d2c04a5ace1c94fa9f93a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 17:39:00 +0000 Subject: [PATCH 0188/1059] Make it clear that JSON array responses can be documented Closes gh-163 --- .../src/docs/asciidoc/documenting-your-api.adoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c7a184cb5..840171c9f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -167,6 +167,23 @@ The following paths are all present: |=== +A response that uses an array at its root can also be documented. The path `[]` will refer +to the entire array. You can then use bracket or dot notation to identify fields within +the array's entries. For example, `[].id` corresponds to the `id` field of every object +found in the following array: + +[source,json,indent=0] +---- + [ + { + "id":1 + }, + { + "id":2 + } + ] +---- + [[documenting-your-api-request-response-payloads-json-field-types]] From e8cf957f1506a2790259e3c9ad6004f69732d8c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 17:50:34 +0000 Subject: [PATCH 0189/1059] Update documentation to mention that you can write custom preprocessors Closes gh-156 --- .../customizing-requests-and-responses.adoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index f88a58795..37ca4e0a9 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -84,4 +84,15 @@ from the request or response. `replacePattern` on `Preprocessors` provides a general purpose mechanism for replacing content in a request or response. Any occurrences of a regular expression are -replaced. \ No newline at end of file +replaced. + +[[customizing-requests-and-responses-preprocessors-writing-your-own]] +==== Writing your own preprocessor + +If one of the built-in preprocessors does not meet your needs, you can write your own by +implementing the `OperationPreprocessor` interface. You can then use your custom +preprocessor in exactly the same way as any of the built-in preprocessors. + +If you only want to modify the content (body) of a request or response, consider +implementing the `ContentModifier` interface and using it with the built-in +`ContentModifyingOperationPreprocessor`. \ No newline at end of file From a3c3bef093fb77ccecd2568a7fa817a1df526f7b Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 29 Oct 2015 20:49:34 +0900 Subject: [PATCH 0190/1059] Fix the link in the documentation to the default snippet templates Closes gh-160 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 840171c9f..25fc52b15 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -521,7 +521,7 @@ sample applications to see this functionality in action. ==== Customizing the generated snippets Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the generated snippets. -{source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +{source}/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates[Default templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. From 88d3e72ae94e38fb927cec000d39b78af0af5a9f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:48:56 +0000 Subject: [PATCH 0191/1059] Upgrade to Spring Framework 4.1.8.RELEASE --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 593cde259..8785ee21b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ sonarRunner { } ext { - springVersion = '4.1.7.RELEASE' + springVersion = '4.1.8.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", From 939769db110505888338a124a4788cd5f5569d50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:50:20 +0000 Subject: [PATCH 0192/1059] Upgrade the samples to Spring Boot 1.2.7.RELEASE --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index aa6384b6c..36aa3ae74 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.6.RELEASE + 1.2.7.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 8dfb61e8a..8e7bd07d8 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' } } From f6f4b77331d438b7f6b8b715c96265584f951299 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:52:19 +0000 Subject: [PATCH 0193/1059] Upgrade the samples to use the latest Asciidoctor plugins --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 36aa3ae74..223ebdfcb 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -73,7 +73,7 @@ org.asciidoctor asciidoctor-maven-plugin - 1.5.2 + 1.5.2.1 generate-docs diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 8e7bd07d8..1f05c5414 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -8,7 +8,7 @@ buildscript { } plugins { - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.convert" version "1.5.3" } apply plugin: 'java' From 437d2bcbce13396da6ec9f707afb43645a12fd41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 21:15:44 +0000 Subject: [PATCH 0194/1059] Uniquely identify the 1.0.x branch in Sonar --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8785ee21b..2e078797c 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ apply plugin: 'sonar-runner' sonarRunner { sonarProperties { + property 'sonar.branch', '1.0.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' From 83e56f68abf7d9a1174151f89f84f4f0d3c3f3b2 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Thu, 10 Dec 2015 01:43:58 -0800 Subject: [PATCH 0195/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 59241404f..a651766f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.1.BUILD-SNAPSHOT +version=1.0.2.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 99a2e4f0e0fddf5576ebb510de9f3b6cea4b74af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 10 Dec 2015 09:55:36 +0000 Subject: [PATCH 0196/1059] Update the samples to build against 1.0.2 snapshots --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 223ebdfcb..6e057717d 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.1.BUILD-SNAPSHOT + 1.0.2.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 1f05c5414..665e88f9a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.1.BUILD-SNAPSHOT' + springRestdocsVersion = '1.0.2.BUILD-SNAPSHOT' } dependencies { From 194e62bde3c1888986e6ba2107c8c300eb89a17c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 17 Jan 2016 20:23:27 +0000 Subject: [PATCH 0197/1059] =?UTF-8?q?Remove=20assumption=20that=20working?= =?UTF-8?q?=20directory=20will=20be=20the=20project=E2=80=99s=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin for building the samples assumed that the current working directory would be the directory containing the project’s build.gradle. While this is generally true on the command line (it isn’t if -project-dir is used), it isn’t true in Gradle’s IDE integration. This was leading to an NPE as the sampleBuild task was being configured to depend on a null task as a result of not finding the sample’s pom.xml or build.grade file. This commit corrects the main build.gradle to configure each sample’s working directory relative to the root project’s directory, rather than relying on the working directory. It also improves SampleBuildConfigurer so that it will throw an exception if it fails to find a sample’s build.gradle or pom.xml rather than continuing and triggering an NPE inside Gradle. Closes gh-185 --- build.gradle | 4 ++-- .../build/SampleBuildConfigurer.groovy | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 2e078797c..743c45bb5 100644 --- a/build.gradle +++ b/build.gradle @@ -156,11 +156,11 @@ samples { dependOn 'spring-restdocs-mockmvc:install' restNotesSpringHateoas { - workingDir 'samples/rest-notes-spring-hateoas' + workingDir "$projectDir/samples/rest-notes-spring-hateoas" } restNotesSpringDataRest { - workingDir 'samples/rest-notes-spring-data-rest' + workingDir "$projectDir/samples/rest-notes-spring-data-rest" } } diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index db94081e1..5ddd17ab1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -38,17 +38,21 @@ public class SampleBuildConfigurer { } Task createTask(Project project, Object... dependencies) { + File sampleDir = new File(this.workingDir).absoluteFile Task verifyIncludes - if (new File(this.workingDir, 'build.gradle').isFile()) { + if (new File(sampleDir, 'build.gradle').isFile()) { Task gradleBuild = createGradleBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'build/asciidoc')) + verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) verifyIncludes.dependsOn gradleBuild } - if (new File(this.workingDir, 'pom.xml').isFile()) { - Task mavenBuild = createMavenBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'target/generated-docs')) + else if (new File(sampleDir, 'pom.xml').isFile()) { + Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) + verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) verifyIncludes.dependsOn(mavenBuild) } + else { + throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") + } Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" @@ -56,12 +60,12 @@ public class SampleBuildConfigurer { return sampleBuild } - private Task createMavenBuild(Project project, Object... dependencies) { + private Task createMavenBuild(Project project, File sampleDir, Object... dependencies) { Task mavenBuild = project.tasks.create("${name}Maven", Exec) mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "${new File(this.workingDir).absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 82a5b96b69532d81e45dd9df4ebebb8528e6afb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Jan 2016 13:28:19 +0000 Subject: [PATCH 0198/1059] Update the default copyright header for 2016 --- config/eclipse/org.eclipse.jdt.ui.prefs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index c5e23c905..681d02ad0 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -70,7 +70,7 @@ org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true From 08b24e0496d65f474c7dfd8961a4e292a41a4bee Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Jan 2016 13:36:20 +0000 Subject: [PATCH 0199/1059] Remove redundant Eclipse metadata --- .../.settings/org.eclipse.jdt.ui.prefs | 125 ------------------ .../.settings/org.eclipse.jdt.ui.prefs | 125 ------------------ 2 files changed, 250 deletions(-) delete mode 100644 spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs delete mode 100644 spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index c5e23c905..000000000 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,125 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring REST Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring REST Docs Java Conventions -formatter_settings_version=12 -org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.keywordthis=false -org.eclipse.jdt.ui.ondemandthreshold=9999 -org.eclipse.jdt.ui.overrideannotation=true -org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=true -sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=false -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=false -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=true -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=true -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=true -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=true -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index c5e23c905..000000000 --- a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,125 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring REST Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring REST Docs Java Conventions -formatter_settings_version=12 -org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.keywordthis=false -org.eclipse.jdt.ui.ondemandthreshold=9999 -org.eclipse.jdt.ui.overrideannotation=true -org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=true -sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=false -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=false -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=true -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=true -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=true -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=true -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true From 02fa42445f7c8c84509bac86d3d1ebe569ca9c8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 11:14:29 +0000 Subject: [PATCH 0200/1059] Make consistent use of the diamond operator with generic types --- config/eclipse/org.eclipse.jdt.core.prefs | 2 +- .../restdocs/curl/CurlRequestSnippet.java | 6 +++--- .../headers/AbstractHeadersSnippet.java | 8 ++++---- .../restdocs/http/HttpRequestSnippet.java | 4 ++-- .../restdocs/http/HttpResponseSnippet.java | 4 ++-- .../restdocs/hypermedia/LinksSnippet.java | 6 +++--- .../payload/AbstractFieldsSnippet.java | 6 +++--- .../restdocs/payload/JsonContentHandler.java | 4 ++-- .../restdocs/payload/JsonFieldProcessor.java | 6 +++--- .../request/AbstractParametersSnippet.java | 6 +++--- .../restdocs/hypermedia/LinksSnippetTests.java | 4 ++-- .../payload/JsonFieldProcessorTests.java | 18 +++++++++--------- .../restdocs/test/OutputCapture.java | 4 ++-- .../restdocs/test/SnippetMatchers.java | 4 ++-- .../restdocs/mockmvc/IterableEnumeration.java | 4 ++-- ...ckMvcRestDocumentationIntegrationTests.java | 5 ++--- 16 files changed, 45 insertions(+), 46 deletions(-) diff --git a/config/eclipse/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs index 5281c3b46..462feac8f 100644 --- a/config/eclipse/org.eclipse.jdt.core.prefs +++ b/config/eclipse/org.eclipse.jdt.core.prefs @@ -71,7 +71,7 @@ org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 56f1fd4ca..3e903f377 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -49,7 +49,7 @@ public class CurlRequestSnippet extends TemplatedSnippet { private static final Set HEADER_FILTERS; static { - Set headerFilters = new HashSet(); + Set headerFilters = new HashSet<>(); headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); headerFilters.add(new BasicAuthHeaderFilter()); @@ -75,7 +75,7 @@ protected CurlRequestSnippet(Map attributes) { @Override protected Map createModel(Operation operation) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("url", getUrl(operation)); model.put("options", getOptions(operation)); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index f5e2c1269..223913567 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -76,7 +76,7 @@ protected Map createModel(Operation operation) { private void validateHeaderDocumentation(Operation operation) { List missingHeaders = findMissingHeaders(operation); if (!missingHeaders.isEmpty()) { - List names = new ArrayList(); + List names = new ArrayList<>(); for (HeaderDescriptor headerDescriptor : missingHeaders) { names.add(headerDescriptor.getName()); } @@ -94,7 +94,7 @@ private void validateHeaderDocumentation(Operation operation) { * @return descriptors for the headers that are missing from the operation */ protected List findMissingHeaders(Operation operation) { - List missingHeaders = new ArrayList(); + List missingHeaders = new ArrayList<>(); Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { if (!headerDescriptor.isOptional() @@ -132,7 +132,7 @@ protected final List getHeaderDescriptors() { * @return the model */ protected Map createModelForDescriptor(HeaderDescriptor descriptor) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); model.put("optional", descriptor.isOptional()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 9060ada91..908b2fb84 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -64,7 +64,7 @@ protected HttpRequestSnippet(Map attributes) { @Override protected Map createModel(Operation operation) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); model.put( "path", diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 9d2de12c3..c9fd1f718 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -58,7 +58,7 @@ protected HttpResponseSnippet(Map attributes) { protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); HttpStatus status = response.getStatus(); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("responseBody", responseBody(response)); model.put("statusCode", status.value()); model.put("statusReason", status.getReasonPhrase()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 79ae0ee26..b7f868c85 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -101,7 +101,7 @@ protected Map createModel(Operation operation) { private void validate(Map> links) { Set actualRels = links.keySet(); - Set undocumentedRels = new HashSet(actualRels); + Set undocumentedRels = new HashSet<>(actualRels); undocumentedRels.removeAll(this.descriptorsByRel.keySet()); Set requiredRels = new HashSet<>(); @@ -112,7 +112,7 @@ private void validate(Map> links) { } } - Set missingRels = new HashSet(requiredRels); + Set missingRels = new HashSet<>(requiredRels); missingRels.removeAll(actualRels); if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 3bf98a83b..3257be17c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -123,7 +123,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { if (message.length() > 0) { message += String.format("%n"); } - List paths = new ArrayList(); + List paths = new ArrayList<>(); for (FieldDescriptor fieldDescriptor : missingFields) { paths.add(fieldDescriptor.getPath()); } @@ -170,7 +170,7 @@ protected final List getFieldDescriptors() { * @return the model */ protected Map createModelForDescriptor(FieldDescriptor descriptor) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("path", descriptor.getPath()); model.put("type", descriptor.getType().toString()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index fab1d1a5c..b08f14279 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -45,7 +45,7 @@ class JsonContentHandler implements ContentHandler { @Override public List findMissingFields(List fieldDescriptors) { - List missingFields = new ArrayList(); + List missingFields = new ArrayList<>(); Object payload = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { if (!fieldDescriptor.isOptional() diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 7ef5df395..5e4ed29d8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -32,7 +32,7 @@ final class JsonFieldProcessor { boolean hasField(JsonFieldPath fieldPath, Object payload) { - final AtomicReference hasField = new AtomicReference(false); + final AtomicReference hasField = new AtomicReference<>(false); traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { @Override @@ -45,7 +45,7 @@ public void foundMatch(Match match) { } Object extract(JsonFieldPath path, Object payload) { - final List matches = new ArrayList(); + final List matches = new ArrayList<>(); traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 0763505b1..7eb6e11c1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -84,9 +84,9 @@ protected Map createModel(Operation operation) { private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); Set expectedParameters = this.descriptorsByName.keySet(); - Set undocumentedParameters = new HashSet(actualParameters); + Set undocumentedParameters = new HashSet<>(actualParameters); undocumentedParameters.removeAll(expectedParameters); - Set missingParameters = new HashSet(expectedParameters); + Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index cb9ac2883..07adce26d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -174,7 +174,7 @@ public void linksWithCustomAttributes() throws IOException { private static class StubLinkExtractor implements LinkExtractor { - private MultiValueMap linksByRel = new LinkedMultiValueMap(); + private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); @Override public MultiValueMap extractLinks(OperationResponse response) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index d48212f00..1f9415979 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -138,22 +138,22 @@ public void nonExistentTopLevelField() { @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedField() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", new HashMap()); this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedFieldWhenParentIsNotAMap() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", 5); this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldWhenParentIsAnArray() { - HashMap payload = new HashMap(); - HashMap alpha = new HashMap(); + HashMap payload = new HashMap<>(); + HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload); @@ -161,21 +161,21 @@ public void nonExistentFieldWhenParentIsAnArray() { @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayField() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayFieldAsTypeDoesNotMatch() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", 5); this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldBeneathAnArray() { - HashMap payload = new HashMap(); - HashMap alpha = new HashMap(); + HashMap payload = new HashMap<>(); + HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); this.fieldProcessor.extract(JsonFieldPath.compile("a.b[].id"), payload); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java index 4aab96685..e7b4ef755 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -45,7 +45,7 @@ public class OutputCapture implements TestRule { private ByteArrayOutputStream capturedOutput; - private List> matchers = new ArrayList>(); + private List> matchers = new ArrayList<>(); @Override public Statement apply(final Statement base, Description description) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index d45388a09..97818a52e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -74,7 +74,7 @@ public static AsciidoctorCodeBlockMatcher codeBlock(String language) { private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { - private List lines = new ArrayList(); + private List lines = new ArrayList<>(); protected void addLine(String line) { this.lines.add(line); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java index d80edb13b..d4b9653f2 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -63,7 +63,7 @@ public void remove() { * @return the iterable */ static Iterable iterable(Enumeration enumeration) { - return new IterableEnumeration(enumeration); + return new IterableEnumeration<>(enumeration); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 546a17e75..76e3dd677 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -430,8 +430,7 @@ public ResponseEntity> foo() { response.put("links", Arrays.asList(new Link("rel", "href"))); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); - return new ResponseEntity>(response, headers, - HttpStatus.OK); + return new ResponseEntity<>(response, headers, HttpStatus.OK); } @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) From d184425216c809aa28d0be54004bc0b9422ca009 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 12:38:19 +0000 Subject: [PATCH 0201/1059] Polishing --- .../customizing-requests-and-responses.adoc | 6 +- ...yTest.java => EveryTestPreprocessing.java} | 45 +++++------ ...PerTest.java => PerTestPreprocessing.java} | 20 +++-- .../PrettyPrintingContentModifier.java | 19 +++-- ...cumentationContextPlaceholderResolver.java | 14 +++- .../snippet/TemplatedSnippetTests.java | 77 +++++++++++++++++++ 6 files changed, 134 insertions(+), 47 deletions(-) rename docs/src/test/java/com/example/{PreprocessingEveryTest.java => EveryTestPreprocessing.java} (83%) rename docs/src/test/java/com/example/{PreprocessingPerTest.java => PerTestPreprocessing.java} (83%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 37ca4e0a9..2bc11527c 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -12,7 +12,7 @@ example: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingPerTest.java[tags=preprocessing] +include::{examples-dir}/com/example/PerTestPreprocessing.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. @@ -24,7 +24,7 @@ output directories>>: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=setup] +include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=setup] ---- <1> Create the `RestDocumentationResultHandler`, configured to preprocess the request and response. @@ -36,7 +36,7 @@ test-specific. For example: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=use] +include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=use] ---- <1> Document the links specific to the resource that is being tested <2> The `perform` call will automatically produce the documentation snippets due to the diff --git a/docs/src/test/java/com/example/PreprocessingEveryTest.java b/docs/src/test/java/com/example/EveryTestPreprocessing.java similarity index 83% rename from docs/src/test/java/com/example/PreprocessingEveryTest.java rename to docs/src/test/java/com/example/EveryTestPreprocessing.java index 6199ecb46..b189f8ffd 100644 --- a/docs/src/test/java/com/example/PreprocessingEveryTest.java +++ b/docs/src/test/java/com/example/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,14 @@ package com.example; +import org.junit.Before; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; @@ -23,42 +31,35 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import org.junit.Before; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +public class EveryTestPreprocessing { -public class PreprocessingEveryTest { - private WebApplicationContext context; - + private MockMvc mockMvc; - + private RestDocumentationResultHandler document; - + // tag::setup[] @Before public void setup() { - this.document = document("{method-name}", // <1> - preprocessRequest(removeHeaders("Foo")), - preprocessResponse(prettyPrint())); - this.mockMvc = MockMvcBuilders - .webAppContextSetup(this.context) - .alwaysDo(this.document) // <2> - .build(); + this.document = document( + "{method-name}", // <1> + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .alwaysDo(this.document) // <2> + .build(); } - // end::setup[] + + // end::setup[] public void use() throws Exception { // tag::use[] this.document.snippets( // <1> links(linkWithRel("self").description("Canonical self link"))); this.mockMvc.perform(get("/")) // <2> - .andExpect(status().isOk()); + .andExpect(status().isOk()); // end::use[] } diff --git a/docs/src/test/java/com/example/PreprocessingPerTest.java b/docs/src/test/java/com/example/PerTestPreprocessing.java similarity index 83% rename from docs/src/test/java/com/example/PreprocessingPerTest.java rename to docs/src/test/java/com/example/PerTestPreprocessing.java index 6f0ab77ff..504f96c09 100644 --- a/docs/src/test/java/com/example/PreprocessingPerTest.java +++ b/docs/src/test/java/com/example/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,27 +16,25 @@ package com.example; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.web.servlet.MockMvc; - -public class PreprocessingPerTest { +public class PerTestPreprocessing { private MockMvc mockMvc; public void general() throws Exception { // tag::preprocessing[] - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index", - preprocessRequest(removeHeaders("Foo")), // <1> - preprocessResponse(prettyPrint()))); // <2> + this.mockMvc.perform(get("/")).andExpect(status().isOk()) + .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 16c9471ac..9665fd09c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -98,9 +98,8 @@ private SAXSource createSaxSource(byte[] original) SAXParser parser = parserFactory.newSAXParser(); XMLReader xmlReader = parser.getXMLReader(); xmlReader.setErrorHandler(new SilentErrorHandler()); - SAXSource xmlInput = new SAXSource(xmlReader, new InputSource( - new ByteArrayInputStream(original))); - return xmlInput; + return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream( + original))); } private static final class SilentErrorListener implements ErrorListener { @@ -108,18 +107,18 @@ private static final class SilentErrorListener implements ErrorListener { @Override public void warning(TransformerException exception) throws TransformerException { - + // Suppress } @Override public void error(TransformerException exception) throws TransformerException { - + // Suppress } @Override public void fatalError(TransformerException exception) throws TransformerException { - + // Suppress } } @@ -128,17 +127,17 @@ private static final class SilentErrorHandler implements ErrorHandler { @Override public void warning(SAXParseException exception) throws SAXException { - + // Suppress } @Override public void error(SAXParseException exception) throws SAXException { - + // Suppress } @Override public void fatalError(SAXParseException exception) throws SAXException { - + // Suppress } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 908f3d5eb..bc12e8725 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -70,6 +70,14 @@ public String resolvePlaceholder(String placeholderName) { if ("step".equals(placeholderName)) { return Integer.toString(this.context.getStepCount()); } + String converted = tryMethodNameConversion(placeholderName); + if (converted != null) { + return converted; + } + return tryClassNameConversion(placeholderName); + } + + private String tryMethodNameConversion(String placeholderName) { if ("methodName".equals(placeholderName)) { return this.context.getTestMethodName(); } @@ -79,6 +87,10 @@ public String resolvePlaceholder(String placeholderName) { if ("method_name".equals(placeholderName)) { return camelCaseToSnakeCase(this.context.getTestMethodName()); } + return null; + } + + private String tryClassNameConversion(String placeholderName) { if ("ClassName".equals(placeholderName)) { return this.context.getTestClass().getSimpleName(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java new file mode 100644 index 000000000..f356d2da3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2016 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.snippet; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.restdocs.operation.Operation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link TemplatedSnippet}. + * + * @author Andy Wilkinson + */ +public class TemplatedSnippetTests { + + @Test + public void attributesAreCopied() { + Map attributes = new HashMap<>(); + attributes.put("a", "alpha"); + TemplatedSnippet snippet = new TestTemplatedSnippet(attributes); + attributes.put("b", "bravo"); + assertThat(snippet.getAttributes().size(), is(1)); + assertThat(snippet.getAttributes(), hasEntry("a", (Object) "alpha")); + } + + @Test + public void nullAttributesAreTolerated() { + assertThat(new TestTemplatedSnippet(null).getAttributes(), is(not(nullValue()))); + assertThat(new TestTemplatedSnippet(null).getAttributes().size(), is(0)); + } + + @Test + public void snippetName() { + assertThat( + new TestTemplatedSnippet(Collections.emptyMap()) + .getSnippetName(), + is(equalTo("test"))); + } + + private static class TestTemplatedSnippet extends TemplatedSnippet { + + protected TestTemplatedSnippet(Map attributes) { + super("test", attributes); + } + + @Override + protected Map createModel(Operation operation) { + return null; + } + + } + +} From 97ae7a9ce9d10e3dddb62196f5b3b17edde81307 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 12:51:37 +0000 Subject: [PATCH 0202/1059] Fix javadoc problem with use of em dash --- .../snippet/RestDocumentationContextPlaceholderResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index bc12e8725..6c3e00be2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -26,7 +26,7 @@ * A {@link PlaceholderResolver} that resolves placeholders using a * {@link RestDocumentationContext}. The following placeholders are supported: *
      - *
    • {@code step} – the {@link RestDocumentationContext#getStepCount() step current + *
    • {@code step} - the {@link RestDocumentationContext#getStepCount() step current * count}. *
    • {@code methodName} - the unmodified name of the * {@link RestDocumentationContext#getTestMethodName() current test method} without From 5f4541bf69ee50ce318878462d93324e77932c6f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Jan 2016 16:55:04 +0000 Subject: [PATCH 0203/1059] Collapse Maven and Gradle setup into one section with switchable examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds an Asciidoctor extension that post-processes code blocks, collapsing any secondary blocks into the preceding primary block. The primary block is then augmented with a switch. This switch contains one item for each block. Clicking an item causes the associated block’s content to be displayed. Each item is named using its block’s title. The getting started instructions have been updated to take advantage of this new switching support, with the Maven and Gradle instructions being collapsed into a single section. Closes gh-189 --- buildSrc/build.gradle | 7 + .../CodeBlockSwitchExtension.groovy | 28 ++++ .../CodeBlockSwitchPostProcessor.groovy | 39 +++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../src/main/resources/codeBlockSwitch.css | 23 +++ .../src/main/resources/codeBlockSwitch.js | 45 +++++ docs/src/docs/asciidoc/getting-started.adoc | 154 ++++++++---------- 7 files changed, 211 insertions(+), 86 deletions(-) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy create mode 100644 buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 buildSrc/src/main/resources/codeBlockSwitch.css create mode 100644 buildSrc/src/main/resources/codeBlockSwitch.js diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..ec63ab98b --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,7 @@ +repositories { + mavenCentral() +} + +dependencies { + compile 'org.asciidoctor:asciidoctorj:1.5.2' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy new file mode 100644 index 000000000..2dbd03649 --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 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.asciidoctor + +import org.asciidoctor.Asciidoctor +import org.asciidoctor.extension.spi.ExtensionRegistry + +class CodeBlockSwitchExtension implements ExtensionRegistry { + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry().postprocessor(new CodeBlockSwitchPostProcessor()); + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy new file mode 100644 index 000000000..e87edf966 --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy @@ -0,0 +1,39 @@ +/* + * 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.asciidoctor + +import org.asciidoctor.ast.Document +import org.asciidoctor.extension.Postprocessor + +class CodeBlockSwitchPostProcessor extends Postprocessor { + + String process(Document document, String output) { + def css = getClass().getResource("/codeBlockSwitch.css").text + def javascript = getClass().getResource("/codeBlockSwitch.js").text + def replacement = """ + + + +""" + return output.replace("", replacement); + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..2dcc8a1d3 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.CodeBlockSwitchExtension \ No newline at end of file diff --git a/buildSrc/src/main/resources/codeBlockSwitch.css b/buildSrc/src/main/resources/codeBlockSwitch.css new file mode 100644 index 000000000..6fb1947b1 --- /dev/null +++ b/buildSrc/src/main/resources/codeBlockSwitch.css @@ -0,0 +1,23 @@ +.hidden { + display: none; +} + +.switch { + border-width: 1px 1px 0 1px; + border-style: solid; + border-color: #7a2518; + display: inline-block; +} + +.switch--item { + padding: 10px; + background-color: #ffffff; + color: #7a2518; + display: inline-block; + cursor: pointer; +} + +.switch--item.selected { + background-color: #7a2519; + color: #ffffff; +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/codeBlockSwitch.js b/buildSrc/src/main/resources/codeBlockSwitch.js new file mode 100644 index 000000000..13ac0fc1e --- /dev/null +++ b/buildSrc/src/main/resources/codeBlockSwitch.js @@ -0,0 +1,45 @@ +function addBlockSwitches() { + $('.primary').each(function() { + primary = $(this); + createSwitchItem(primary, createBlockSwitch(primary)).item.addClass("selected"); + primary.children('.title').remove(); + }); + $('.secondary').each(function(idx, node) { + secondary = $(node); + primary = findPrimary(secondary); + switchItem = createSwitchItem(secondary, primary.children('.switch')); + switchItem.content.addClass('hidden'); + findPrimary(secondary).append(switchItem.content); + secondary.remove(); + }); +} + +function createBlockSwitch(primary) { + blockSwitch = $('
      '); + primary.prepend(blockSwitch); + return blockSwitch; +} + +function findPrimary(secondary) { + candidate = secondary.prev(); + while (!candidate.is('.primary')) { + candidate = candidate.prev(); + } + return candidate; +} + +function createSwitchItem(block, blockSwitch) { + blockName = block.children('.title').text(); + content = block.children('.content').first().append(block.next('.colist')); + item = $('
      ' + blockName + '
      '); + item.on('click', '', content, function(e) { + $(this).addClass('selected'); + $(this).siblings().removeClass('selected'); + e.data.siblings('.content').addClass('hidden'); + e.data.removeClass('hidden'); + }); + blockSwitch.append(item); + return {'item': item, 'content': content}; +} + +$(addBlockSwitches); \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 6a652de67..ccc0f7a59 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -28,82 +28,14 @@ The code that produces the generated snippets can be found in `src/test/java`. [[getting-started-build-configuration]] === Build configuration -The first step in using Spring REST Docs is to configure your project's build. - - - -[[getting-started-build-configuration-gradle]] -==== Gradle build configuration - -The {samples}/rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` -file that you may wish to use as a reference. The key parts of the configuration are -described below. - -[source,groovy,indent=0,subs="verbatim,attributes"] ----- - plugins { <1> - id "org.asciidoctor.convert" version "1.5.2" - } - - dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' - } - - ext { <3> - snippetsDir = file('build/generated-snippets') - } - - test { <4> - outputs.dir snippetsDir - } - - asciidoctor { <5> - attributes 'snippets': snippetsDir <6> - inputs.dir snippetsDir <7> - dependsOn test <8> - } ----- -<1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. -<3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task to add the snippets directory as an output. -<5> Configure the `asciidoctor` task -<6> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<7> Configure the snippets directory as an input. -<8> Make the task depend on the test task so that the tests are run before the - documentation is created. - - -[[getting-started-build-configuration-gradle-packaging-the-documentation]] -==== Packaging the documentation - -You may want to package the generated documentation in your project's jar file, for -example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as -static content] by Spring Boot. You can do so by configuring the `jar` task to depend on -the `asciidoctor` task and to copy the generated documentation into the jar's static -directory: - -[source,groovy,indent=0] ----- - jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } - } ----- - - - -[[getting-started-build-configuration-maven]] -==== Maven build configuration - -The {samples}/rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` -file that you may wish to use as a reference. The key parts of the configuration are -described below. - -[source,xml,indent=0,subs="verbatim,attributes"] +The first step in using Spring REST Docs is to configure your project's build. The +{samples}/rest-notes-spring-hateoas[Spring HATEOAS] and +{samples}/rest-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` +and `pom.xml` respectively that you may wish to use as a reference. The key parts of +the configuration are described below. + +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven ---- <1> org.springframework.restdocs @@ -150,7 +82,6 @@ described below. - ---- <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. <2> Configure a property to define the output location for generated snippets. @@ -163,19 +94,55 @@ described below. <> in your project's jar you should use the `prepare-package` phase. -[[getting-started-build-configuration-maven-packaging]] +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- + plugins { <1> + id "org.asciidoctor.convert" version "1.5.2" + } + + dependencies { <2> + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' + } + + ext { <3> + snippetsDir = file('build/generated-snippets') + } + + test { <4> + outputs.dir snippetsDir + } + + asciidoctor { <5> + attributes 'snippets': snippetsDir <6> + inputs.dir snippetsDir <7> + dependsOn test <8> + } +---- +<1> Apply the Asciidoctor plugin. +<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. +<3> Configure a property to define the output location for generated snippets. +<4> Configure the `test` task to add the snippets directory as an output. +<5> Configure the `asciidoctor` task +<6> Define an attribute named `snippets` that can be used when including the generated + snippets in your documentation. +<7> Configure the snippets directory as an input. +<8> Make the task depend on the test task so that the tests are run before the + documentation is created. + + +[[getting-started-build-configuration-gradle-packaging-the-documentation]] ==== Packaging the documentation You may want to package the generated documentation in your project's jar file, for example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as -static content] by Spring Boot. +static content] by Spring Boot. To do so, configure your project's build so that: -First, configure the Asciidoctor plugin so that it runs in the `prepare-package` phase, as -<>. Now configure -Maven's resources plugin to copy the generated documentation into a location where it'll -be included in the project's jar: +1. The documentation is generated before the jar is built +2. The generated documentation is included in the jar -[source,xml,indent=0] +[source,xml,indent=0,role="primary",role="primary"] +.Maven ---- <1> org.asciidoctor @@ -192,7 +159,7 @@ be included in the project's jar: copy-resources - + <3> ${project.build.outputDirectory}/static/docs @@ -211,7 +178,22 @@ be included in the project's jar: <1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound to the same phase (`prepare-package`) and the resource plugin must run after the -Asciidoctor plugin. +Asciidoctor plugin to ensure that the documentation is generated before it's copied. +<3> Copy the generated documentation into the build output's `static/docs` directory, +from where it will be included in the jar file. + +[source,groovy,indent=0,role="secondary"] +.Gradle +---- + jar { + dependsOn asciidoctor <1> + from ("${asciidoctor.outputDir}/html5") { <2> + into 'static/docs' + } + } +---- +<1> Ensure that the documentation has been generated before the jar is built. +<2> Copy the generated documentation into the jar's `static/docs` directory. From d55b5b7738f1487a991ee341b71431184707376e Mon Sep 17 00:00:00 2001 From: Grant Hutchins Date: Wed, 27 Jan 2016 09:37:37 -0600 Subject: [PATCH 0204/1059] Fix typo Closes gh-188 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 6a652de67..0232564be 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -283,7 +283,7 @@ a `RestDocumentationResultHandler`. An instance of this class can be obtained fr static `document` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. -By default, three snippets a written: +By default, three snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` From 9dfa0b538509b1336a0afa3f26e3fe750d93a7cb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jan 2016 11:15:45 +0000 Subject: [PATCH 0205/1059] Polish PatternReplacingContentModifier --- .../PatternReplacingContentModifier.java | 16 ++++++++-------- .../PatternReplacingContentModifierTests.java | 13 +++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 58d4e3a7a..7fe67754c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -35,8 +35,8 @@ class PatternReplacingContentModifier implements ContentModifier { private final String replacement; /** - * Creates a new {@link PatternReplacingContentModifier} that will replace occurences - * the given {@code pattern} with the given {@code replacement}. + * Creates a new {@link PatternReplacingContentModifier} that will replace occurrences + * of the given {@code pattern} with the given {@code replacement}. * * @param pattern the pattern * @param replacement the replacement @@ -56,7 +56,7 @@ public byte[] modifyContent(byte[] content, MediaType contentType) { original = new String(content); } Matcher matcher = this.pattern.matcher(original); - StringBuilder buffer = new StringBuilder(); + StringBuilder builder = new StringBuilder(); int previous = 0; while (matcher.find()) { String prefix; @@ -68,13 +68,13 @@ public byte[] modifyContent(byte[] content, MediaType contentType) { prefix = original.substring(previous, matcher.start()); previous = matcher.end(); } - buffer.append(prefix); - buffer.append(this.replacement); + builder.append(prefix); + builder.append(this.replacement); } if (previous < original.length()) { - buffer.append(original.substring(previous)); + builder.append(original.substring(previous)); } - return buffer.toString().getBytes(); + return builder.toString().getBytes(); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 1046cad9a..9d37ed78e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -46,6 +46,19 @@ public void patternsAreReplaced() throws Exception { is(equalTo("{\"id\" : \"<>\"}".getBytes()))); } + @Test + public void contentThatDoesNotMatchIsUnchanged() throws Exception { + Pattern pattern = Pattern.compile( + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + Pattern.CASE_INSENSITIVE); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( + pattern, "<>"); + assertThat( + contentModifier.modifyContent( + "{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes(), null), + is(equalTo("{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes()))); + } + @Test public void encodingIsPreserved() { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; From 130b411e2ad5e5305b8248ad672a97ab9439c353 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Sep 2015 14:36:42 +0100 Subject: [PATCH 0206/1059] Add support for using REST Assured to generate documentation snippets This commit adds a new module, spring-restdocs-restassured, that can be used to generate documentation snippets when testing a service with REST Assured. Please refer to the updated reference documentation for details. Thanks to Johan Haleby for making a change to REST Assured so that path parameters could be documented. Closes gh-102 --- build.gradle | 1 + config/checkstyle/checkstyle.xml | 2 +- docs/build.gradle | 1 + docs/src/docs/asciidoc/configuration.adoc | 41 +- .../customizing-requests-and-responses.adoc | 66 +++- .../docs/asciidoc/documenting-your-api.adoc | 168 ++++++-- docs/src/docs/asciidoc/getting-started.adoc | 77 ++-- docs/src/docs/asciidoc/introduction.adoc | 7 +- .../test/java/com/example/Constraints.java | 1 + .../CustomDefaultSnippets.java} | 10 +- .../example/{ => mockmvc}/CustomEncoding.java | 10 +- .../{ => mockmvc}/CustomUriConfiguration.java | 6 +- .../{ => mockmvc}/EveryTestPreprocessing.java | 15 +- .../ExampleApplicationTests.java | 13 +- .../example/{ => mockmvc}/HttpHeaders.java | 30 +- .../com/example/{ => mockmvc}/Hypermedia.java | 4 +- .../example/{ => mockmvc}/InvokeService.java | 4 +- .../ParameterizedOutput.java} | 24 +- .../example/{ => mockmvc}/PathParameters.java | 4 +- .../com/example/{ => mockmvc}/Payload.java | 22 +- .../{ => mockmvc}/PerTestPreprocessing.java | 6 +- .../{ => mockmvc}/RequestParameters.java | 6 +- .../restassured/CustomDefaultSnippets.java | 46 +++ .../example/restassured/CustomEncoding.java | 45 +++ .../restassured/EveryTestPreprocessing.java | 72 ++++ .../restassured/ExampleApplicationTests.java | 44 +++ .../com/example/restassured/HttpHeaders.java | 51 +++ .../com/example/restassured/Hypermedia.java | 54 +++ .../example/restassured/InvokeService.java | 38 ++ .../restassured/ParameterizedOutput.java | 46 +++ .../example/restassured/PathParameters.java | 41 ++ .../java/com/example/restassured/Payload.java | 75 ++++ .../restassured/PerTestPreprocessing.java | 44 +++ .../restassured/RequestParameters.java | 53 +++ settings.gradle | 3 +- .../restdocs/config}/AbstractConfigurer.java | 18 +- .../config/AbstractNestedConfigurer.java | 43 ++ .../restdocs/config}/NestedConfigurer.java | 12 +- .../config/RestDocumentationConfigurer.java | 132 +++++++ .../restdocs/config}/SnippetConfigurer.java | 49 ++- .../request/PathParametersSnippet.java | 7 +- .../mockmvc/MockMvcRestDocumentation.java | 6 +- .../MockMvcRestDocumentationConfigurer.java | 115 ++++++ ...rer.java => MockMvcSnippetConfigurer.java} | 27 +- .../RestDocumentationMockMvcConfigurer.java | 198 ---------- .../RestDocumentationResultHandler.java | 27 +- .../restdocs/mockmvc/UriConfigurer.java | 30 +- ...kMvcRestDocumentationConfigurerTests.java} | 16 +- ...kMvcRestDocumentationIntegrationTests.java | 8 +- spring-restdocs-restassured/build.gradle | 16 + .../RestAssuredOperationRequestFactory.java | 108 +++++ .../RestAssuredOperationResponseFactory.java | 53 +++ .../RestAssuredRestDocumentation.java | 108 +++++ ...estAssuredRestDocumentationConfigurer.java | 76 ++++ .../RestAssuredSnippetConfigurer.java | 48 +++ .../restassured/RestDocumentationFilter.java | 164 ++++++++ .../preprocess/RestAssuredPreprocessors.java | 48 +++ .../UriModifyingOperationPreprocessor.java | 249 ++++++++++++ .../operation/preprocess/package-info.java | 22 ++ .../restdocs/restassured/package-info.java | 21 + ...stAssuredOperationRequestFactoryTests.java | 251 ++++++++++++ ...suredRestDocumentationConfigurerTests.java | 91 +++++ ...uredRestDocumentationIntegrationTests.java | 374 ++++++++++++++++++ ...riModifyingOperationPreprocessorTests.java | 343 ++++++++++++++++ .../src/test/resources/body.txt | 1 + .../restdocs/templates/curl-request.snippet | 1 + 66 files changed, 3360 insertions(+), 432 deletions(-) rename docs/src/test/java/com/example/{CustomDefaultSnippetsConfiguration.java => mockmvc/CustomDefaultSnippets.java} (86%) rename docs/src/test/java/com/example/{ => mockmvc}/CustomEncoding.java (87%) rename docs/src/test/java/com/example/{ => mockmvc}/CustomUriConfiguration.java (94%) rename docs/src/test/java/com/example/{ => mockmvc}/EveryTestPreprocessing.java (84%) rename docs/src/test/java/com/example/{ => mockmvc}/ExampleApplicationTests.java (85%) rename docs/src/test/java/com/example/{ => mockmvc}/HttpHeaders.java (63%) rename docs/src/test/java/com/example/{ => mockmvc}/Hypermedia.java (96%) rename docs/src/test/java/com/example/{ => mockmvc}/InvokeService.java (93%) rename docs/src/test/java/com/example/{AlwaysDo.java => mockmvc/ParameterizedOutput.java} (84%) rename docs/src/test/java/com/example/{ => mockmvc}/PathParameters.java (94%) rename docs/src/test/java/com/example/{ => mockmvc}/Payload.java (83%) rename docs/src/test/java/com/example/{ => mockmvc}/PerTestPreprocessing.java (90%) rename docs/src/test/java/com/example/{ => mockmvc}/RequestParameters.java (96%) create mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java create mode 100644 docs/src/test/java/com/example/restassured/CustomEncoding.java create mode 100644 docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTests.java create mode 100644 docs/src/test/java/com/example/restassured/HttpHeaders.java create mode 100644 docs/src/test/java/com/example/restassured/Hypermedia.java create mode 100644 docs/src/test/java/com/example/restassured/InvokeService.java create mode 100644 docs/src/test/java/com/example/restassured/ParameterizedOutput.java create mode 100644 docs/src/test/java/com/example/restassured/PathParameters.java create mode 100644 docs/src/test/java/com/example/restassured/Payload.java create mode 100644 docs/src/test/java/com/example/restassured/PerTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParameters.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/AbstractConfigurer.java (59%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/NestedConfigurer.java (72%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/SnippetConfigurer.java (61%) create mode 100644 spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{AbstractNestedConfigurer.java => MockMvcSnippetConfigurer.java} (65%) delete mode 100644 spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java rename spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/{RestDocumentationConfigurerTests.java => MockMvcRestDocumentationConfigurerTests.java} (85%) create mode 100644 spring-restdocs-restassured/build.gradle create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java create mode 100644 spring-restdocs-restassured/src/test/resources/body.txt create mode 100644 spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index 30c5a2b05..a286bdeed 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ subprojects { } dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.jayway.restassured:rest-assured:2.8.0' dependency 'com.samskivert:jmustache:1.11' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b7908d746..ebc91d6ff 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/build.gradle b/docs/build.gradle index 5241018c0..2816cd284 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { testCompile project(':spring-restdocs-mockmvc') + testCompile project(':spring-restdocs-restassured') testCompile 'javax.validation:validation-api' } diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ae1b601f8..239588003 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -6,7 +6,12 @@ [[configuration-uris]] === Documented URIs -The default configuration for URIs documented by Spring REST Docs is: +NOTE: As REST Assured tests a service by making actual HTTP requests, the documented +URIs cannot be customized in this way. You should use the +<> instead. + +When using MockMvc, the default configuration for URIs documented by Spring REST Docs is: |=== |Setting |Default @@ -21,12 +26,12 @@ The default configuration for URIs documented by Spring REST Docs is: |`8080` |=== -This configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use its API +This configuration is applied by `MockMvcRestDocumentationConfigurer`. You can use its API to change one or more of the defaults to suit your needs: [source,java,indent=0] ---- -include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] +include::{examples-dir}/com/example/mockmvc/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- TIP: To configure a request's context path, use the `contextPath` method on @@ -38,12 +43,19 @@ TIP: To configure a request's context path, use the `contextPath` method on === Snippet encoding The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same -default for the snippets that it generates. If you require an encoding other than `UTF-8`, -use `RestDocumentationMockMvcConfigurer` to configure it: +default for the snippets that it generates. You can change the default snippet encoding +using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-encoding] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] +include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom-encoding] ---- @@ -57,11 +69,18 @@ Three snippets are produced by default: - `http-request` - `http-response` -This default configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use -its API to change the configuration. For example, to only produce the `curl-request` +You can change the default snippet configuration during setup using the +`RestDocumentationConfigurer` API. For example, to only produce the `curl-request` snippet by default: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=custom-default-snippets] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/CustomDefaultSnippetsConfiguration.java[tags=custom-default-snippets] +include::{examples-dir}/com/example/restassured/CustomDefaultSnippets.java[tags=custom-default-snippets] ---- \ No newline at end of file diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 2bc11527c..900106388 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -10,9 +10,18 @@ and/or an `OperationResponsePreprocessor`. Instances can be obtained using the static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/PerTestPreprocessing.java[tags=preprocessing] +include::{examples-dir}/com/example/mockmvc/PerTestPreprocessing.java[tags=preprocessing] +---- +<1> Apply a request preprocessor that will remove the header named `Foo`. +<2> Apply a response preprocessor that will pretty print its content. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. @@ -22,25 +31,44 @@ so by configuring the preprocessors in your `@Before` method and using the <>: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=setup] +include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=setup] ---- -<1> Create the `RestDocumentationResultHandler`, configured to preprocess the request +<1> Create a `RestDocumentationResultHandler`, configured to preprocess the request and response. -<2> Create the `MockMvc` instance, configured to always call the documentation result +<2> Create a `MockMvc` instance, configured to always call the documentation result handler. -Then, in each test, the `RestDocumentationResultHandler` can be configured with anything -test-specific. For example: +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=setup] +---- +<1> Create a `RestDocumentationFilter`, configured to preprocess the request + and response. +<2> Create a `RequestSpecification` instance, configured to always call the documentation + filter. + +Then, in each test, any configuration specific to that test can be performed. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use] +---- +<1> Document the links specific to the resource that is being tested +<2> The request and response will be preprocessed due to the use of `alwaysDo` above. -[source,java,indent=0] +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=use] +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ---- <1> Document the links specific to the resource that is being tested -<2> The `perform` call will automatically produce the documentation snippets due to the - use of `alwaysDo` above. +<2> The request and response will be preprocessed due to the configuration of the +`RequestSpecification` in the `setUp` method. Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See <> for further details. @@ -86,6 +114,20 @@ from the request or response. replacing content in a request or response. Any occurrences of a regular expression are replaced. + + +[[customizing-requests-and-responses-preprocessors-modify-uris]] +==== Modifying URIs + +TIP: If you are using MockMvc, URIs should be customized by <>. + +`modifyUris` on `RestAssuredPreprocessors` can be used to modify any URIs in a request +or a response. When using REST Assured, this allows you to customize the URIs that appear +in the documentation while testing a local instance of the service. + + + [[customizing-requests-and-responses-preprocessors-writing-your-own]] ==== Writing your own preprocessor diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 25fc52b15..c68b9d43a 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -11,9 +11,22 @@ This section provides more details about using Spring REST Docs to document your Spring REST Docs provides support for documenting the links in a https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=links] +---- +<1> Configure Spring REST docs to produce a snippet describing the response's links. + Uses the static `links` method on + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<3> Expect a link whose rel is `bravo`. + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/Hypermedia.java[tag=links] +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] ---- <1> Configure Spring REST docs to produce a snippet describing the response's links. Uses the static `links` method on @@ -47,14 +60,23 @@ Two link formats are understood by default: If you are using Atom or HAL-format links but with a different content type you can provide one of the built-in `LinkExtractor` implementations to `links`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] +include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=explicit-extractor] ---- <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -If your API represents its links in a format other than Atom or HAL you can provide your +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=explicit-extractor] +---- +<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. + +If your API represents its links in a format other than Atom or HAL, you can provide your own implementation of the `LinkExtractor` interface to extract the links from the response. @@ -65,9 +87,22 @@ In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also provided. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Payload.java[tags=response] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the response + payload. To document a request `requestFields` can be used. Both are static methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.email`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=response] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on @@ -223,9 +258,17 @@ The type can also be set explicitly using the `type(Object)` method on in the documentation. Typically, one of the values enumerated by `JsonFieldType` will be used: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Payload.java[tags=explicit-type] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=explicit-type] +---- +<1> Set the field's type to `string`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] ---- <1> Set the field's type to `string`. @@ -255,9 +298,10 @@ method will be used in the documentation. A request's parameters can be documented using `requestParameters`. Request parameters can be included in a `GET` request's query string. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-query-string] ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. @@ -268,14 +312,37 @@ include::{examples-dir}/com/example/RequestParameters.java[tags=request-paramete `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. + Uses the static `requestParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `page` parameter. Uses the static `parameterWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `per_page` parameter. +<4> Perform a `GET` request with two parameters, `page` and `per_page` in the query + string. + Request parameters can also be included as form data in the body of a POST request: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-form-data] ---- <1> Perform a `POST` request with a single parameter, `username`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] +---- +<1> Configure the `username` parameter. +<2> Perform the `POST` request. + In both cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. @@ -294,9 +361,10 @@ above. A request's path parameters can be documented using `pathParameters`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] +include::{examples-dir}/com/example/mockmvc/PathParameters.java[tags=path-parameters] ---- <1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. <2> Configure Spring REST Docs to produce a snippet describing the request's path @@ -306,6 +374,19 @@ include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-parameters] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's path + parameters. Uses the static `pathParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the parameter named `latitude`. Uses the static `parameterWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the parameter named `longitude`. +<4> Perform a `GET` request with two path parameters, `latitude` and `longitude`. + The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. @@ -329,9 +410,10 @@ above. The headers in a request or response can be documented using `requestHeaders` and `responseHeaders` respectively. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] +include::{examples-dir}/com/example/mockmvc/HttpHeaders.java[tags=headers] ---- <1> Perform a `GET` request with an `Authorization` header that uses basic authentication <2> Configure Spring REST Docs to produce a snippet describing the request's headers. @@ -342,6 +424,20 @@ include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/HttpHeaders.java[tags=headers] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's headers. + Uses the static `requestHeaders` method on + `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Document the `Authorization` header. Uses the static `headerWithName` method on + `org.springframework.restdocs.headers.HeaderDocumentation. +<3> Produce a snippet describing the response's headers. Uses the static `responseHeaders` + method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Configure the request with an `Authorization` header that uses basic authentication + The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -436,8 +532,7 @@ class in the Spring HATEOAS-based sample illustrates the latter approach. [[documenting-your-api-default-snippets]] === Default snippets -A number of snippets are produced automatically when you document a call to -`MockMvc.perform`: +A number of snippets are produced automatically when you document a request and response. [cols="1,3"] |=== @@ -489,23 +584,30 @@ are supported: | The nome of the test class, formatted using snake_case | {step} -| The count of calls to MockMvc.perform in the current test +| The count of calls made to the service in the current test |=== For example, `document("{class-name}/{method-name}")` in a test method named `creatingANote` on the test class `GettingStartedDocumentation`, will write snippets into a directory named `getting-started-documentation/creating-a-note`. -A parameterized output directory is particularly useful in combination with Spring MVC -Test's `alwaysDo` functionality. It allows documentation to be configured once in a setup -method: +A parameterized output directory is particularly useful in combination with an `@Before` +method. It allows documentation to be configured once in a setup method and then reused +in every test in the class: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/AlwaysDo.java[tags=always-do] +include::{examples-dir}/com/example/mockmvc/ParameterizedOutput.java[tags=parameterized-output] ---- -With this configuration in place, every call to `MockMvc.perform` will produce +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ParameterizedOutput.java[tags=parameterized-output] +---- + +With this configuration in place, every call to the service you are testing will produce the <> without any further configuration. Take a look at the `GettingStartedDocumentation` classes in each of the sample applications to see this functionality in action. @@ -549,9 +651,19 @@ A concrete example of the above is the addition of a constraints column and a ti documenting request fields. The first step is to provide a `constraints` attribute for each field that you are documenting and to provide a `title` attribute: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=constraints] +---- +<1> Configure the `title` attribute for the request fields snippet +<2> Set the `constraints` attribute for the `name` field +<3> Set the `constraints` attribute for the `email` field + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/Payload.java[tags=constraints] +include::{examples-dir}/com/example/restassured/Payload.java[tags=constraints] ---- <1> Configure the `title` attribute for the request fields snippet <2> Set the `constraints` attribute for the `name` field diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8f8a11ca3..2dc9bc92c 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -83,7 +83,9 @@ the configuration are described below. ---- -<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. +<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use + REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` + instead. <2> Configure a property to define the output location for generated snippets. <3> Add the SureFire plugin and configure it to include files whose names end with `Documentation.java`. @@ -120,7 +122,9 @@ project's jar you should use the `prepare-package` phase. } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. +<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If + you want to use REST Assured rather than MockMvc, add a dependency on + `spring-restdocs-restassured` instead. <3> Configure a property to define the output location for generated snippets. <4> Configure the `test` task to add the snippets directory as an output. <5> Configure the `asciidoctor` task @@ -199,14 +203,16 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] === Generating documentation snippets -Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test -framework] to make requests to the service that you are documenting. It then produces -documentation snippets for request and resulting response. +Spring REST Docs uses JUnit and +{spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or +http://www.rest-assured.io[REST Assured] to make requests to the service that you are +documenting. It then produces documentation snippets for the request and the resulting +response. [[getting-started-documentation-snippets-setup]] -==== Setting up Spring MVC test +==== Setting up your tests The first step in generating documentation snippets is to declare a `public` `RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` @@ -214,47 +220,59 @@ rule is configured with the output directory into which generated snippets shoul written. This output directory should match the snippets directory that you have configured in your `build.gradle` or `pom.xml` file. -For Maven (`pom.xml` that will typically be `target/generated-snippets`: +For Maven (`pom.xml` that will typically be `target/generated-snippets` and for +Gradle (`build.gradle`) it will typically be `build/generated-snippets`: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.Maven ---- @Rule public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); ---- -And for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: - -[source,java,indent=0] +[source,java,indent=0,role="secondary"] +.Gradle ---- @Rule public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); ---- -Next, provide an `@Before` method that creates a `MockMvc` instance: +Next, provide an `@Before` method to configure MockMvc or REST Assured: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTests.java[tags=setup] ---- - -The `MockMvc` instance is configured using a `RestDocumentationMockMvcConfigurer`. An +<1> The `MockMvc` instance is configured using a `MockMvcRestDocumentationConfigurer`. An instance of this class can be obtained from the static `documentationConfiguration()` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. -`RestDocumentationMockMvcConfigurer` applies sensible defaults and also provides an API -for customizing the configuration. Refer to the -<> for more information. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTests.java[tags=setup] +---- +<1> REST Assured is configured by adding a `RestAssuredRestDocumentationConfigurer` as a +`Filter`. An instance of this class can be obtained from the static +`documentationConfiguration()` method on +`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. + +The configurer applies sensible defaults and also provides an API for customizing the +configuration. Refer to the <> for more information. [[getting-started-documentation-snippets-invoking-the-service]] ==== Invoking the RESTful service -Now that a `MockMvc` instance has been created, it can be used to invoke the RESTful +Now that the testing framework has been configured, it can be used to invoke the RESTful service and document the request and response. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/InvokeService.java[tags=invoke-service] +include::{examples-dir}/com/example/mockmvc/InvokeService.java[tags=invoke-service] ---- <1> Invoke the root (`/`) of the service and indicate that an `application/json` response is required. @@ -265,6 +283,21 @@ a `RestDocumentationResultHandler`. An instance of this class can be obtained fr static `document` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-service] +---- +<1> Apply the specification that was initialised in the `@Before` method. +<2> Indicate that an `application/json` response is required. +<3> Document the call to the service, writing the snippets into a directory named `index` +that will be located beneath the configured output directory. The snippets are written by +a `RestDocumentationFilter`. An instance of this class can be obtained from the +static `document` method on +`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. +<4> Invoke the root (`/`) of the service. +<5> Assert that the service produce the expected response. + By default, three snippets are written: * `/index/curl-request.adoc` diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 9590ac358..3bfaef7d3 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -10,9 +10,10 @@ http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produc HTML, styled and layed out to suit your needs. Spring REST Docs makes use of snippets produced by tests written with -{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test]. This test-driven -approach helps to guarantee the accuracy of your service's documentation. If a snippet is -incorrect the test that produces it will fail. +{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or +http://www.rest-assured.io[REST Assured]. This test-driven approach helps to guarantee the +accuracy of your service's documentation. If a snippet is incorrect the test that produces +it will fail. Documenting a RESTful service is largely about describing its resources. Two key parts of each resource's description are the details of the HTTP requests that it consumes diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java index 5bcea8595..254a9577b 100644 --- a/docs/src/test/java/com/example/Constraints.java +++ b/docs/src/test/java/com/example/Constraints.java @@ -41,6 +41,7 @@ static class UserInput { @NotNull @Size(min = 8) String password; + } // end::constraints[] diff --git a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java similarity index 86% rename from docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java rename to docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index a964c9646..fcd6a742a 100644 --- a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -public class CustomDefaultSnippetsConfiguration { +public class CustomDefaultSnippets { @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); @@ -41,8 +41,8 @@ public class CustomDefaultSnippetsConfiguration { public void setUp() { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withDefaults(curlRequest())) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java similarity index 87% rename from docs/src/test/java/com/example/CustomEncoding.java rename to docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 4490e8a4d..80130ef18 100644 --- a/docs/src/test/java/com/example/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.web.context.WebApplicationContext; public class CustomEncoding { - + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); @@ -40,8 +40,8 @@ public class CustomEncoding { public void setUp() { // tag::custom-encoding[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withEncoding("ISO-8859-1")) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java similarity index 94% rename from docs/src/test/java/com/example/CustomUriConfiguration.java rename to docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index b70d4e08e..9fe6130c9 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.web.context.WebApplicationContext; public class CustomUriConfiguration { - + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); diff --git a/docs/src/test/java/com/example/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java similarity index 84% rename from docs/src/test/java/com/example/EveryTestPreprocessing.java rename to docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index b189f8ffd..e1f9b9961 100644 --- a/docs/src/test/java/com/example/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -25,6 +27,7 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; @@ -34,20 +37,24 @@ public class EveryTestPreprocessing { + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + private WebApplicationContext context; + // tag::setup[] private MockMvc mockMvc; private RestDocumentationResultHandler document; - // tag::setup[] @Before public void setup() { - this.document = document( - "{method-name}", // <1> + this.document = document("{method-name}", // <1> preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(this.document) // <2> .build(); } diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java similarity index 85% rename from docs/src/test/java/com/example/ExampleApplicationTests.java rename to docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 70fb8e188..97abfa059 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.junit.Before; import org.junit.Rule; @@ -29,8 +29,9 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); - // tag::mock-mvc-setup[] + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + // tag::setup[] @Autowired private WebApplicationContext context; @@ -39,8 +40,8 @@ public class ExampleApplicationTests { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) + .apply(documentationConfiguration(this.restDocumentation)) // <1> .build(); } - // end::mock-mvc-setup[] + // end::setup[] } diff --git a/docs/src/test/java/com/example/HttpHeaders.java b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java similarity index 63% rename from docs/src/test/java/com/example/HttpHeaders.java rename to docs/src/test/java/com/example/mockmvc/HttpHeaders.java index f47e39714..e31ddbf80 100644 --- a/docs/src/test/java/com/example/HttpHeaders.java +++ b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.springframework.test.web.servlet.MockMvc; @@ -32,19 +32,19 @@ public class HttpHeaders { public void headers() throws Exception { // tag::headers[] this.mockMvc - .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> - .andExpect(status().isOk()) - .andDo(document("headers", - requestHeaders( // <2> - headerWithName("Authorization").description( - "Basic auth credentials")), // <3> - responseHeaders( // <4> - headerWithName("X-RateLimit-Limit").description( - "The total number of requests permitted per period"), - headerWithName("X-RateLimit-Remaining").description( - "Remaining requests permitted in current period"), - headerWithName("X-RateLimit-Reset").description( - "Time at which the rate limit period will reset")))); + .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + .andExpect(status().isOk()) + .andDo(document("headers", + requestHeaders( // <2> + headerWithName("Authorization").description( + "Basic auth credentials")), // <3> + responseHeaders( // <4> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))); // end::headers[] } } diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/mockmvc/Hypermedia.java similarity index 96% rename from docs/src/test/java/com/example/Hypermedia.java rename to docs/src/test/java/com/example/mockmvc/Hypermedia.java index 3cf6c1bcf..6358fa95e 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/mockmvc/Hypermedia.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/mockmvc/InvokeService.java similarity index 93% rename from docs/src/test/java/com/example/InvokeService.java rename to docs/src/test/java/com/example/mockmvc/InvokeService.java index 097899f2b..2eb8eeb5c 100644 --- a/docs/src/test/java/com/example/InvokeService.java +++ b/docs/src/test/java/com/example/mockmvc/InvokeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; diff --git a/docs/src/test/java/com/example/AlwaysDo.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java similarity index 84% rename from docs/src/test/java/com/example/AlwaysDo.java rename to docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index efc64f6c7..ea6313cd1 100644 --- a/docs/src/test/java/com/example/AlwaysDo.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,10 +14,7 @@ * limitations under the License. */ -package com.example; - -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +package com.example.mockmvc; import org.junit.Before; import org.junit.Rule; @@ -26,24 +23,25 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -public class AlwaysDo { - +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public class ParameterizedOutput { + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); private MockMvc mockMvc; private WebApplicationContext context; - - - // tag::always-do[] + // tag::parameterized-output[] @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")) - .build(); + .alwaysDo(document("{method-name}/{step}/")).build(); } - // end::always-do[] + // end::parameterized-output[] + } diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/mockmvc/PathParameters.java similarity index 94% rename from docs/src/test/java/com/example/PathParameters.java rename to docs/src/test/java/com/example/mockmvc/PathParameters.java index c78105986..460d37dbc 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/mockmvc/PathParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java similarity index 83% rename from docs/src/test/java/com/example/Payload.java rename to docs/src/test/java/com/example/mockmvc/Payload.java index 5ee1a8902..bed9663f1 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -51,7 +51,6 @@ public void explicitType() throws Exception { .andDo(document("index", responseFields( fieldWithPath("contact.email") .type(JsonFieldType.STRING) // <1> - .optional() .description("The user's email address")))); // end::explicit-type[] } @@ -61,16 +60,13 @@ public void constraints() throws Exception { .andExpect(status().isOk()) // tag::constraints[] .andDo(document("create-user", requestFields( - attributes( - key("title").value("Fields for user creation")), // <1> - fieldWithPath("name") - .description("The user's name") - .attributes( - key("constraints").value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email") - .description("The user's email address") - .attributes( - key("constraints").value("Must be a valid email address"))))); // <3> + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))); // <3> // end::constraints[] } diff --git a/docs/src/test/java/com/example/PerTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java similarity index 90% rename from docs/src/test/java/com/example/PerTestPreprocessing.java rename to docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java index 504f96c09..60e56da18 100644 --- a/docs/src/test/java/com/example/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.springframework.test.web.servlet.MockMvc; @@ -33,8 +33,8 @@ public class PerTestPreprocessing { public void general() throws Exception { // tag::preprocessing[] this.mockMvc.perform(get("/")).andExpect(status().isOk()) - .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> - preprocessResponse(prettyPrint()))); // <2> + .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/docs/src/test/java/com/example/RequestParameters.java b/docs/src/test/java/com/example/mockmvc/RequestParameters.java similarity index 96% rename from docs/src/test/java/com/example/RequestParameters.java rename to docs/src/test/java/com/example/mockmvc/RequestParameters.java index 5a314958c..eece34115 100644 --- a/docs/src/test/java/com/example/RequestParameters.java +++ b/docs/src/test/java/com/example/mockmvc/RequestParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -39,7 +39,7 @@ public void getQueryStringSnippet() throws Exception { ))); // end::request-parameters-query-string[] } - + public void postFormDataSnippet() throws Exception { // tag::request-parameters-form-data[] this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java new file mode 100644 index 000000000..4166c13e0 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomDefaultSnippets { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-default-snippets[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withDefaults(curlRequest())) + .build(); + // end::custom-default-snippets[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java new file mode 100644 index 000000000..eb85c8059 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomEncoding { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-encoding[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withEncoding("ISO-8859-1")) + .build(); + // end::custom-encoding[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java new file mode 100644 index 000000000..68b8a88b1 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.restassured.RestDocumentationFilter; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class EveryTestPreprocessing { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + private RestDocumentationFilter document; + + @Before + public void setup() { + this.document = document("{method-name}", + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); // <1> + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(this.document)// <2> + .build(); + } + + // end::setup[] + + public void use() throws Exception { + // tag::use[] + this.document.snippets( // <1> + links(linkWithRel("self").description("Canonical self link"))); + RestAssured.given(this.spec) // <2> + .when().get("/") + .then().assertThat().statusCode(is(200)); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java new file mode 100644 index 000000000..c0943a777 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTests { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + @Before + public void setUp() { + this.spec = new RequestSpecBuilder().addFilter( + documentationConfiguration(this.restDocumentation)) // <1> + .build(); + } + // end::setup[] +} diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java new file mode 100644 index 000000000..2556be02e --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpHeaders.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class HttpHeaders { + + private RequestSpecification spec; + + public void headers() throws Exception { + // tag::headers[] + RestAssured.given(this.spec) + .filter(document("headers", + requestHeaders( // <1> + headerWithName("Authorization").description( + "Basic auth credentials")), // <2> + responseHeaders( // <3> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))) + .header("Authroization", "Basic dXNlcjpzZWNyZXQ=") // <4> + .when().get("/people") + .then().assertThat().statusCode(is(200)); + // end::headers[] + } +} diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java new file mode 100644 index 000000000..2922377d8 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Hypermedia.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class Hypermedia { + + private RequestSpecification spec; + + public void defaultExtractor() throws Exception { + // tag::links[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("index", links( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), // <2> + linkWithRel("bravo").description("Link to the bravo resource")))) // <3> + .get("/").then().assertThat().statusCode(is(200)); + // end::links[] + } + + public void explicitExtractor() throws Exception { + RestAssured.given(this.spec) + .accept("application/json") + // tag::explicit-extractor[] + .filter(document("index", links(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + // end::explicit-extractor[] + .get("/").then().assertThat().statusCode(is(200)); + } + +} diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java new file mode 100644 index 000000000..347c368ef --- /dev/null +++ b/docs/src/test/java/com/example/restassured/InvokeService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class InvokeService { + + private RequestSpecification spec; + + public void invokeService() throws Exception { + // tag::invoke-service[] + RestAssured.given(this.spec) // <1> + .accept("application/json") // <2> + .filter(document("index")) // <3> + .when().get("/") // <4> + .then().assertThat().statusCode(is(200)); // <5> + // end::invoke-service[] + } +} diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java new file mode 100644 index 000000000..10fd8be1d --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ParameterizedOutput { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + private RequestSpecification spec; + + // tag::parameterized-output[] + @Before + public void setUp() { + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}/{step}")).build(); + } + // end::parameterized-output[] + +} diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java new file mode 100644 index 000000000..81c7cc3e4 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PathParameters.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PathParameters { + + private RequestSpecification spec; + + public void pathParametersSnippet() throws Exception { + // tag::path-parameters[] + RestAssured.given(this.spec) + .filter(document("locations", pathParameters( // <1> + parameterWithName("latitude").description("The location's latitude"), // <2> + parameterWithName("longitude").description("The location's longitude")))) // <3> + .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> + .then().assertThat().statusCode(is(200)); + // end::path-parameters[] + } +} diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java new file mode 100644 index 000000000..ee6c2456f --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.springframework.restdocs.payload.JsonFieldType; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +public class Payload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::response[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("user", responseFields( // <1> + fieldWithPath("contact").description("The user's contact details"), // <2> + fieldWithPath("contact.email").description("The user's email address")))) // <3> + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + // end::response[] + } + + public void explicitType() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::explicit-type[] + .filter(document("user", responseFields( + fieldWithPath("contact.email") + .type(JsonFieldType.STRING) // <1> + .description("The user's email address")))) + // end::explicit-type[] + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + } + + public void constraints() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::constraints[] + .filter(document("create-user", requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))) // <3> + // end::constraints[] + .when().post("/users") + .then().assertThat().statusCode(is(200)); + } + +} diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java new file mode 100644 index 000000000..69f4936ef --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PerTestPreprocessing { + + private RequestSpecification spec; + + public void general() throws Exception { + // tag::preprocessing[] + RestAssured.given(this.spec) + .filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))) // <2> + .when().get("/") + .then().assertThat().statusCode(is(200)); + // end::preprocessing[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/RequestParameters.java new file mode 100644 index 000000000..86f40c4be --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParameters.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParameters { + + private RequestSpecification spec; + + public void getQueryStringSnippet() throws Exception { + // tag::request-parameters-query-string[] + RestAssured.given(this.spec) + .filter(document("users", requestParameters( // <1> + parameterWithName("page").description("The page to retrieve"), // <2> + parameterWithName("per_page").description("Entries per page")))) // <3> + .when().get("/users?page=2&per_page=100") // <4> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-query-string[] + } + + public void postFormDataSnippet() throws Exception { + // tag::request-parameters-form-data[] + RestAssured.given(this.spec) + .filter(document("create-user", requestParameters( + parameterWithName("username").description("The user's username")))) + .formParam("username", "Tester") // <1> + .when().post("/users") // <2> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-form-data[] + } + +} diff --git a/settings.gradle b/settings.gradle index 7552e239c..8d6619a19 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'spring-restdocs' include 'docs' include 'spring-restdocs-core' -include 'spring-restdocs-mockmvc' \ No newline at end of file +include 'spring-restdocs-mockmvc' +include 'spring-restdocs-restassured' \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java similarity index 59% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index f8effd0a9..7a8a9e03e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; +package org.springframework.restdocs.config; -import org.springframework.mock.web.MockHttpServletRequest; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; /** * Abstract configurer that declares methods that are internal to the documentation @@ -24,13 +26,15 @@ * * @author Andy Wilkinson */ -abstract class AbstractConfigurer { +public abstract class AbstractConfigurer { /** - * Applies the configuration, possibly by modifying the given {@code request}. + * Applies the configurer to the given {@code configuration}. * - * @param request the request that may be modified + * @param configuration the configuration to be configured + * @param context the current documentation context */ - abstract void apply(MockHttpServletRequest request); + public abstract void apply(Map configuration, + RestDocumentationContext context); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java new file mode 100644 index 000000000..1463edde4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2016 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.config; + +/** + * Base class for {@link NestedConfigurer} implementations. + * + * @param

      The type of the configurer's parent + * @author Andy Wilkinson + */ +public abstract class AbstractNestedConfigurer

      extends AbstractConfigurer implements + NestedConfigurer

      { + + private final P parent; + + /** + * Creates a new {@code AbstractNestedConfigurer} with the given {@code parent}. + * @param parent the parent + */ + protected AbstractNestedConfigurer(P parent) { + this.parent = parent; + } + + @Override + public final P and() { + return this.parent; + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java similarity index 72% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java index 1c6768c8f..72e2f41bf 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,22 +14,20 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; - -import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +package org.springframework.restdocs.config; /** * A configurer that is nested and, therefore, has a parent. * - * @param The parent's type + * @param

      The parent's type * @author Andy Wilkinson */ -interface NestedConfigurer { +interface NestedConfigurer

      { /** * Returns the configurer's parent. * * @return the parent */ - PARENT and(); + P and(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java new file mode 100644 index 000000000..bedec85ae --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -0,0 +1,132 @@ +/* + * Copyright 2014-2016 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.config; + +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +/** + * Abstract base class for the configuration of Spring REST Docs. + * + * @param The concrete type of the {@link SnippetConfigurer}. + * @param The concrete type of this configurer, to be returned from methods that + * support chaining + * @author Andy Wilkinson + */ +public abstract class RestDocumentationConfigurer { + + private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); + + private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + + /** + * Returns a {@link SnippetConfigurer} that can be used to configure the snippets that + * will be generated. + * + * @return the snippet configurer + */ + public abstract S snippets(); + + /** + * Configures the {@link TemplateEngine} that will be used for snippet rendering. + * + * @param templateEngine the template engine to use + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public final T templateEngine(TemplateEngine templateEngine) { + this.templateEngineConfigurer.setTemplateEngine(templateEngine); + return (T) this; + } + + /** + * Configures the {@link WriterResolver} that will be used to resolve a writer for a + * snippet. + * + * @param writerResolver The writer resolver to use + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public final T writerResolver(WriterResolver writerResolver) { + this.writerResolverConfigurer.setWriterResolver(writerResolver); + return (T) this; + } + + /** + * Returns the configurer used to configure the context with a {@link WriterResolver}. + * + * @return the configurer + */ + protected final AbstractConfigurer getWriterResolverConfigurer() { + return this.writerResolverConfigurer; + } + + /** + * Returns the configurer used to configure the context with a {@link TemplateEngine}. + * + * @return the configurer + */ + protected final AbstractConfigurer getTemplateEngineConfigurer() { + return this.templateEngineConfigurer; + } + + private static final class TemplateEngineConfigurer extends AbstractConfigurer { + + private TemplateEngine templateEngine = new MustacheTemplateEngine( + new StandardTemplateResourceResolver()); + + @Override + public void apply(Map configuration, + RestDocumentationContext context) { + configuration.put(TemplateEngine.class.getName(), this.templateEngine); + } + + private void setTemplateEngine(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + } + + private static final class WriterResolverConfigurer extends AbstractConfigurer { + + private WriterResolver writerResolver; + + @Override + public void apply(Map configuration, + RestDocumentationContext context) { + WriterResolver resolverToUse = this.writerResolver; + if (resolverToUse == null) { + resolverToUse = new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolver(context)); + } + configuration.put(WriterResolver.class.getName(), resolverToUse); + } + + private void setWriterResolver(WriterResolver writerResolver) { + this.writerResolver = writerResolver; + } + + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java similarity index 61% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 2723d0166..6f4be5a54 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; +package org.springframework.restdocs.config; import java.util.Arrays; import java.util.List; +import java.util.Map; -import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -28,10 +29,16 @@ /** * A configurer that can be used to configure the generated documentation snippets. * + * @param

      The type of the configurer's parent + * @param The concrete type of the configurer to be returned from chained methods * @author Andy Wilkinson */ -public class SnippetConfigurer extends - AbstractNestedConfigurer { +public abstract class SnippetConfigurer extends AbstractNestedConfigurer

      { + + /** + * The name of the attribute that is used to hold the default snippets. + */ + public static final String ATTRIBUTE_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; private List defaultSnippets = Arrays.asList( CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), @@ -46,10 +53,22 @@ public class SnippetConfigurer extends private String snippetEncoding = DEFAULT_SNIPPET_ENCODING; - SnippetConfigurer(RestDocumentationMockMvcConfigurer parent) { + /** + * Creates a new {@code SnippetConfigurer} with the given {@code parent}. + * + * @param parent the parent + */ + protected SnippetConfigurer(P parent) { super(parent); } + @Override + public void apply(Map configuration, RestDocumentationContext context) { + ((WriterResolver) configuration.get(WriterResolver.class.getName())) + .setEncoding(this.snippetEncoding); + configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); + } + /** * Configures any documentation snippets to be written using the given * {@code encoding}. The default is UTF-8. @@ -57,17 +76,10 @@ public class SnippetConfigurer extends * @param encoding the encoding * @return {@code this} */ - public SnippetConfigurer withEncoding(String encoding) { + @SuppressWarnings("unchecked") + public T withEncoding(String encoding) { this.snippetEncoding = encoding; - return this; - } - - @Override - void apply(MockHttpServletRequest request) { - ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) - .setEncoding(this.snippetEncoding); - request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", - this.defaultSnippets); + return (T) this; } /** @@ -76,8 +88,9 @@ void apply(MockHttpServletRequest request) { * @param defaultSnippets the default snippets * @return {@code this} */ - public SnippetConfigurer withDefaults(Snippet... defaultSnippets) { + @SuppressWarnings("unchecked") + public T withDefaults(Snippet... defaultSnippets) { this.defaultSnippets = Arrays.asList(defaultSnippets); - return this; + return (T) this; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 5df99f6f6..0670be1da 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -92,8 +92,9 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes().get( "org.springframework.restdocs.urlTemplate"); - Assert.notNull(urlTemplate, - "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " + Assert.notNull( + urlTemplate, + "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " + "build the request?"); return urlTemplate; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index bedd3753a..541f380d9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -44,9 +44,9 @@ private MockMvcRestDocumentation() { * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) */ - public static RestDocumentationMockMvcConfigurer documentationConfiguration( + public static MockMvcRestDocumentationConfigurer documentationConfiguration( RestDocumentation restDocumentation) { - return new RestDocumentationMockMvcConfigurer(restDocumentation); + return new MockMvcRestDocumentationConfigurer(restDocumentation); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java new file mode 100644 index 000000000..f9beacace --- /dev/null +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2016 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.mockmvc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; + +/** + * A MockMvc-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class MockMvcRestDocumentationConfigurer + extends + RestDocumentationConfigurer + implements MockMvcConfigurer { + + private final MockMvcSnippetConfigurer snippetConfigurer = new MockMvcSnippetConfigurer( + this); + + private final UriConfigurer uriConfigurer = new UriConfigurer(this); + + private final RestDocumentation restDocumentation; + + MockMvcRestDocumentationConfigurer(RestDocumentation restDocumentation) { + super(); + this.restDocumentation = restDocumentation; + } + + /** + * Returns a {@link UriConfigurer} that can be used to configure the request URIs that + * will be documented. + * + * @return the URI configurer + */ + public UriConfigurer uris() { + return this.uriConfigurer; + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, + Arrays.asList(getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), snippets(), this.uriConfigurer)); + } + + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + // Nothing to do + } + + @Override + public MockMvcSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + private static final class ConfigurerApplyingRequestPostProcessor implements + RequestPostProcessor { + + private final RestDocumentation restDocumentation; + + private final List configurers; + + private ConfigurerApplyingRequestPostProcessor( + RestDocumentation restDocumentation, List configurers) { + this.restDocumentation = restDocumentation; + this.configurers = configurers; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + RestDocumentationContext context = this.restDocumentation.beforeOperation(); + request.setAttribute(RestDocumentationContext.class.getName(), context); + Map configuration = new HashMap<>(); + configuration.put(MockHttpServletRequest.class.getName(), request); + String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; + configuration.put(urlTemplateAttribute, + request.getAttribute(urlTemplateAttribute)); + request.setAttribute("org.springframework.restdocs.configuration", + configuration); + for (AbstractConfigurer configurer : this.configurers) { + configurer.apply(configuration, context); + } + return request; + } + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java similarity index 65% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java index a4444e7cb..a46302c01 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,40 +16,33 @@ package org.springframework.restdocs.mockmvc; +import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.web.context.WebApplicationContext; /** - * Base class for {@link NestedConfigurer} implementations. + * A configurer that can be used to configure the generated documentation snippets. * - * @param The type of the configurer's parent * @author Andy Wilkinson */ -abstract class AbstractNestedConfigurer extends - AbstractConfigurer implements NestedConfigurer, MockMvcConfigurer { +public class MockMvcSnippetConfigurer extends + SnippetConfigurer + implements MockMvcConfigurer { - private final PARENT parent; - - protected AbstractNestedConfigurer(PARENT parent) { - this.parent = parent; - } - - @Override - public PARENT and() { - return this.parent; + MockMvcSnippetConfigurer(MockMvcRestDocumentationConfigurer parent) { + super(parent); } @Override public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { - this.parent.afterConfigurerAdded(builder); + and().afterConfigurerAdded(builder); } @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return this.parent.beforeMockMvcCreated(builder, context); + return and().beforeMockMvcCreated(builder, context); } - } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java deleted file mode 100644 index 20f06771c..000000000 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ /dev/null @@ -1,198 +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.mockmvc; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; -import org.springframework.restdocs.http.HttpDocumentation; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; -import org.springframework.restdocs.snippet.StandardWriterResolver; -import org.springframework.restdocs.snippet.WriterResolver; -import org.springframework.restdocs.templates.StandardTemplateResourceResolver; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.test.web.servlet.request.RequestPostProcessor; -import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; -import org.springframework.test.web.servlet.setup.MockMvcConfigurer; -import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.web.context.WebApplicationContext; - -/** - * A {@link MockMvcConfigurer} that can be used to configure the documentation. - *

      - * In the absence of any {@link #snippets() customization} the following snippets will be - * produced by default: - *

        - *
      • {@link CurlDocumentation#curlRequest() Curl request}
      • - *
      • {@link HttpDocumentation#httpRequest() HTTP request}
      • - *
      • {@link HttpDocumentation#httpResponse() HTTP response}
      • - *
      - *

      - * In the absence of any {@link #uris() customization}, documented URIs have the following - * defaults: - *

        - *
      • Scheme: {@code http}
      • - *
      • Host: {@code localhost}
      • - *
      • Port: {@code 8080}
      • - *
      - * - * @author Andy Wilkinson - * @author Dmitriy Mayboroda - * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) - * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) - */ -public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter { - - private final UriConfigurer uriConfigurer = new UriConfigurer(this); - - private final SnippetConfigurer snippetConfigurer = new SnippetConfigurer(this); - - private final RequestPostProcessor requestPostProcessor; - - private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); - - private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); - - /** - * Creates a new {code RestDocumentationMockMvcConfigurer} that will use the given - * {@code restDocumentation} when configuring MockMvc. - * - * @param restDocumentation the rest documentation - * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) - */ - RestDocumentationMockMvcConfigurer(RestDocumentation restDocumentation) { - this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( - restDocumentation, this.uriConfigurer, this.writerResolverConfigurer, - this.snippetConfigurer, this.templateEngineConfigurer); - } - - /** - * Returns a {@link UriConfigurer} that can be used to configure the request URIs that - * will be documented. - * - * @return the URI configurer - */ - public UriConfigurer uris() { - return this.uriConfigurer; - } - - /** - * Returns a {@link SnippetConfigurer} that can be used to configure the snippets that - * will be generated. - * - * @return the snippet configurer - */ - public SnippetConfigurer snippets() { - return this.snippetConfigurer; - } - - /** - * Configures the {@link TemplateEngine} that will be used for snippet rendering. - * - * @param templateEngine the template engine to use - * @return {@code this} - */ - public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine templateEngine) { - this.templateEngineConfigurer.setTemplateEngine(templateEngine); - return this; - } - - /** - * Configures the {@link WriterResolver} that will be used to resolve a writer for a - * snippet. - * - * @param writerResolver The writer resolver to use - * @return {@code this} - */ - public RestDocumentationMockMvcConfigurer writerResolver(WriterResolver writerResolver) { - this.writerResolverConfigurer.setWriterResolver(writerResolver); - return this; - } - - @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return this.requestPostProcessor; - } - - private static final class TemplateEngineConfigurer extends AbstractConfigurer { - - private TemplateEngine templateEngine = new MustacheTemplateEngine( - new StandardTemplateResourceResolver()); - - @Override - void apply(MockHttpServletRequest request) { - request.setAttribute(TemplateEngine.class.getName(), this.templateEngine); - } - - void setTemplateEngine(TemplateEngine templateEngine) { - this.templateEngine = templateEngine; - } - - } - - private static final class WriterResolverConfigurer extends AbstractConfigurer { - - private WriterResolver writerResolver; - - @Override - void apply(MockHttpServletRequest request) { - WriterResolver resolverToUse = this.writerResolver; - if (resolverToUse == null) { - resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver( - (RestDocumentationContext) request - .getAttribute(RestDocumentationContext.class - .getName()))); - } - request.setAttribute(WriterResolver.class.getName(), resolverToUse); - } - - void setWriterResolver(WriterResolver writerResolver) { - this.writerResolver = writerResolver; - } - - } - - private static final class ConfigurerApplyingRequestPostProcessor implements - RequestPostProcessor { - - private final RestDocumentation restDocumentation; - - private final AbstractConfigurer[] configurers; - - private ConfigurerApplyingRequestPostProcessor( - RestDocumentation restDocumentation, AbstractConfigurer... configurers) { - this.restDocumentation = restDocumentation; - this.configurers = configurers; - } - - @Override - public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - request.setAttribute(RestDocumentationContext.class.getName(), - this.restDocumentation.beforeOperation()); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(request); - } - return request; - } - - } - -} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 425cd2fc0..075c162fb 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -33,8 +35,6 @@ import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.Assert; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; - /** * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. * @@ -85,9 +85,15 @@ public class RestDocumentationResultHandler implements ResultHandler { @Override public void handle(MvcResult result) throws Exception { Map attributes = new HashMap<>(); - for (String name : iterable(result.getRequest().getAttributeNames())) { - attributes.put(name, result.getRequest().getAttribute(name)); - } + attributes.put(RestDocumentationContext.class.getName(), result.getRequest() + .getAttribute(RestDocumentationContext.class.getName())); + attributes.put("org.springframework.restdocs.urlTemplate", result.getRequest() + .getAttribute("org.springframework.restdocs.urlTemplate")); + @SuppressWarnings("unchecked") + Map configuration = (Map) result.getRequest() + .getAttribute("org.springframework.restdocs.configuration"); + attributes.putAll(configuration); + OperationRequest request = this.requestPreprocessor .preprocess(new MockMvcOperationRequestFactory() .createOperationRequest(result.getRequest())); @@ -103,7 +109,7 @@ public void handle(MvcResult result) throws Exception { } /** - * Adds the given {@code snippets} such that that are documented when this result + * Adds the given {@code snippets} such that they are documented when this result * handler is called. * * @param snippets the snippets to add @@ -116,9 +122,10 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result - .getRequest().getAttribute( - "org.springframework.restdocs.mockmvc.defaultSnippets")); + List combinedSnippets = new ArrayList<>( + (List) ((Map) result.getRequest().getAttribute( + "org.springframework.restdocs.configuration")) + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); combinedSnippets.addAll(this.snippets); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index b7024fd4c..b2c3a8c81 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,7 +16,15 @@ package org.springframework.restdocs.mockmvc; +import java.util.Map; + import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractNestedConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; /** * A configurer that can be used to configure the documented URIs. @@ -24,7 +32,8 @@ * @author Andy Wilkinson */ public class UriConfigurer extends - AbstractNestedConfigurer { + AbstractNestedConfigurer implements + MockMvcConfigurer { /** * The default scheme for documented URIs. @@ -53,7 +62,7 @@ public class UriConfigurer extends private int port = DEFAULT_PORT; - UriConfigurer(RestDocumentationMockMvcConfigurer parent) { + UriConfigurer(MockMvcRestDocumentationConfigurer parent) { super(parent); } @@ -94,10 +103,23 @@ public UriConfigurer withPort(int port) { } @Override - void apply(MockHttpServletRequest request) { + public void apply(Map configuration, RestDocumentationContext context) { + MockHttpServletRequest request = (MockHttpServletRequest) configuration + .get(MockHttpServletRequest.class.getName()); request.setScheme(this.scheme); request.setServerPort(this.port); request.setServerName(this.host); } + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + and().afterConfigurerAdded(builder); + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return and().beforeMockMvcCreated(builder, context); + } + } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java similarity index 85% rename from spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java rename to spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index 486bfd8e6..ad6ac2691 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -33,12 +33,12 @@ import static org.junit.Assert.assertThat; /** - * Tests for {@link RestDocumentationMockMvcConfigurer}. + * Tests for {@link MockMvcRestDocumentationConfigurer}. * * @author Andy Wilkinson * @author Dmitriy Mayboroda */ -public class RestDocumentationConfigurerTests { +public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @@ -47,7 +47,7 @@ public class RestDocumentationConfigurerTests { @Test public void defaultConfiguration() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -56,7 +56,7 @@ public void defaultConfiguration() { @Test public void customScheme() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withScheme("https") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -66,7 +66,7 @@ public void customScheme() { @Test public void customHost() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withHost("api.example.com") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -76,7 +76,7 @@ public void customHost() { @Test public void customPort() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -86,7 +86,7 @@ public void customPort() { @Test public void noContentLengthHeaderWhenRequestHasNotContent() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 76e3dd677..d444dd177 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -83,7 +83,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Integration tests for using Spring REST Docs with Spring Test's Mock MVC. + * Integration tests for using Spring REST Docs with Spring Test's MockMvc. * * @author Andy Wilkinson * @author Dewet Diener @@ -112,8 +112,10 @@ public void clearOutputDirSystemProperty() { @Test public void basicSnippetGeneration() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .snippets().withEncoding("UTF-8")).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic")); diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle new file mode 100644 index 000000000..e7c33f79c --- /dev/null +++ b/spring-restdocs-restassured/build.gradle @@ -0,0 +1,16 @@ +dependencies { + compile project(':spring-restdocs-core') + compile 'com.jayway.restassured:rest-assured' + + testCompile 'org.mockito:mockito-core' + testCompile 'org.hamcrest:hamcrest-library' + testCompile 'org.springframework.hateoas:spring-hateoas' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE' + testCompile 'org.springframework:spring-test' + testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') + testRuntime 'commons-logging:commons-logging:1.2' +} + +test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" +} \ No newline at end of file diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java new file mode 100644 index 000000000..c3ef0b20c --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.Parameters; + +import com.jayway.restassured.response.Header; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.MultiPartSpecification; + +/** + * A factory for creating an {@link OperationRequest} derived from a REST Assured + * {@link FilterableRequestSpecification}. + * + * @author Andy Wilkinson + */ +class RestAssuredOperationRequestFactory { + + OperationRequest createOperationRequest(FilterableRequestSpecification requestSpec) { + return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), + HttpMethod.valueOf(requestSpec.getMethod().name()), + extractContent(requestSpec), extractHeaders(requestSpec), + extractParameters(requestSpec), extractParts(requestSpec)); + } + + private byte[] extractContent(FilterableRequestSpecification requestSpec) { + return convertContent(requestSpec.getBody()); + } + + private byte[] convertContent(Object content) { + if (content instanceof String) { + return ((String) content).getBytes(); + } + else if (content instanceof byte[]) { + return (byte[]) content; + } + else if (content == null) { + return new byte[0]; + } + else { + throw new IllegalStateException("Unsupported request content: " + + content.getClass().getName()); + } + } + + private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : requestSpec.getHeaders()) { + httpHeaders.add(header.getName(), header.getValue()); + } + return httpHeaders; + } + + private Parameters extractParameters(FilterableRequestSpecification requestSpec) { + Parameters parameters = new Parameters(); + for (Entry entry : requestSpec.getQueryParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + for (Entry entry : requestSpec.getRequestParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + for (Entry entry : requestSpec.getFormParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + return parameters; + } + + private Collection extractParts( + FilterableRequestSpecification requestSpec) { + List parts = new ArrayList<>(); + for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(multiPartSpec.getMimeType() == null ? MediaType.TEXT_PLAIN + : MediaType.parseMediaType(multiPartSpec.getMimeType())); + parts.add(new OperationRequestPartFactory().create( + multiPartSpec.getControlName(), multiPartSpec.getFileName(), + convertContent(multiPartSpec.getContent()), headers)); + } + return parts; + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java new file mode 100644 index 000000000..bbcd251ee --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; + +import com.jayway.restassured.response.Header; +import com.jayway.restassured.response.Response; + +/** + * A factory for creating an {@link OperationResponse} derived from a REST Assured + * {@link Response}. + * + * @author Andy Wilkinson + */ +class RestAssuredOperationResponseFactory { + + OperationResponse createOperationResponse(Response response) { + return new OperationResponseFactory().create( + HttpStatus.valueOf(response.getStatusCode()), extractHeaders(response), + extractContent(response)); + } + + private HttpHeaders extractHeaders(Response response) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : response.getHeaders()) { + httpHeaders.add(header.getName(), header.getValue()); + } + return httpHeaders; + } + + private byte[] extractContent(Response response) { + return response.getBody().asByteArray(); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java new file mode 100644 index 000000000..447ba1f55 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting RESTful APIs using REST Assured. + * + * @author Andy Wilkinson + */ +public abstract class RestAssuredRestDocumentation { + + private RestAssuredRestDocumentation() { + + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. + * + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets that will document the API call + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, Snippet... snippets) { + return new RestDocumentationFilter(identifier, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. + * + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, requestPreprocessor, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. + * + * @param identifier an identifier for the API call that is being documented + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, responsePreprocessor, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. + * + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, requestPreprocessor, + responsePreprocessor, snippets); + } + + /** + * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be + * used to configure Spring REST Docs using the given {@code restDocumentation}. + * + * @param restDocumentation the REST documentation + * @return the configurer + */ + public static RestAssuredRestDocumentationConfigurer documentationConfiguration( + RestDocumentation restDocumentation) { + return new RestAssuredRestDocumentationConfigurer(restDocumentation); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java new file mode 100644 index 000000000..5dbdb1c09 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A REST Assured-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public final class RestAssuredRestDocumentationConfigurer + extends + RestDocumentationConfigurer + implements Filter { + + private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( + this); + + private final List configurers; + + private final RestDocumentation restDocumentation; + + RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { + this.restDocumentation = restDocumentation; + this.configurers = Arrays.asList(getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), snippets()); + } + + @Override + public RestAssuredSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext filterContext) { + RestDocumentationContext context = this.restDocumentation.beforeOperation(); + filterContext.setValue(RestDocumentationContext.class.getName(), context); + Map configuration = new HashMap<>(); + filterContext.setValue("org.springframework.restdocs.configuration", + configuration); + for (AbstractConfigurer configurer : this.configurers) { + configurer.apply(configuration, context); + } + return filterContext.next(requestSpec, responseSpec); + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java new file mode 100644 index 000000000..d91b38a18 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.restdocs.config.SnippetConfigurer; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A configurer that can be used to configure the generated documentation snippets when + * using REST Assured. + * + * @author Andy Wilkinson + */ +public final class RestAssuredSnippetConfigurer + extends + SnippetConfigurer + implements Filter { + + RestAssuredSnippetConfigurer(RestAssuredRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + return and().filter(requestSpec, responseSpec, context); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java new file mode 100644 index 000000000..3460a99a3 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A REST Assured {@link Filter} for documenting RESTful APIs. + * + * @author Andy Wilkinson + */ +public final class RestDocumentationFilter implements Filter { + + private final String identifier; + + private final OperationRequestPreprocessor requestPreprocessor; + + private final OperationResponsePreprocessor responsePreprocessor; + + private final List snippets; + + RestDocumentationFilter(String identifier, Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationFilter(String identifier, + OperationRequestPreprocessor operationRequestPreprocessor, + Snippet... snippets) { + this(identifier, operationRequestPreprocessor, + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationFilter(String identifier, + OperationResponsePreprocessor operationResponsePreprocessor, + Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + operationResponsePreprocessor, snippets); + } + + RestDocumentationFilter(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + this.identifier = identifier; + this.requestPreprocessor = requestPreprocessor; + this.responsePreprocessor = responsePreprocessor; + this.snippets = new ArrayList<>(Arrays.asList(snippets)); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + Response response = context.next(requestSpec, responseSpec); + + OperationRequest operationRequest = this.requestPreprocessor + .preprocess(new RestAssuredOperationRequestFactory() + .createOperationRequest(requestSpec)); + OperationResponse operationResponse = this.responsePreprocessor + .preprocess(new RestAssuredOperationResponseFactory() + .createOperationResponse(response)); + + RestDocumentationContext documentationContext = context + .getValue(RestDocumentationContext.class.getName()); + + Map attributes = new HashMap<>(); + attributes.put(RestDocumentationContext.class.getName(), documentationContext); + attributes.put("org.springframework.restdocs.urlTemplate", + requestSpec.getUserDefinedPath()); + Map configuration = context + .getValue("org.springframework.restdocs.configuration"); + attributes.putAll(configuration); + + Operation operation = new StandardOperation(this.identifier, operationRequest, + operationResponse, attributes); + + try { + for (Snippet snippet : getSnippets(configuration)) { + snippet.document(operation); + } + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + + return response; + } + + /** + * Adds the given {@code snippets} such that they are documented when this result + * handler is called. + * + * @param snippets the snippets to add + * @return this {@code RestDocumentationFilter} + */ + public RestDocumentationFilter snippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + return this; + } + + @SuppressWarnings("unchecked") + private List getSnippets(Map configuration) { + List combinedSnippets = new ArrayList<>( + (List) configuration + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); + combinedSnippets.addAll(this.snippets); + return combinedSnippets; + } + + private static final class IdentityOperationRequestPreprocessor implements + OperationRequestPreprocessor { + + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + } + + private static final class IdentityOperationResponsePreprocessor implements + OperationResponsePreprocessor { + + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java new file mode 100644 index 000000000..5e5c1f9f9 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * Static factory methods for creating + * {@link org.springframework.restdocs.operation.preprocess.OperationPreprocessor + * OperationPreprocessors} for use with REST Assured. They can be applied to an + * {@link Operation Operation's} {@link OperationRequest request} or + * {@link OperationResponse response} before it is documented. + * + * @author Andy Wilkinson + */ +public abstract class RestAssuredPreprocessors { + + private RestAssuredPreprocessors() { + + } + + /** + * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the + * request or response by changing one or more of their host, scheme, and port. + * + * @return the preprocessor + */ + public static UriModifyingOperationPreprocessor modifyUris() { + return new UriModifyingOperationPreprocessor(); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java new file mode 100644 index 000000000..bb134d0cc --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.preprocess.ContentModifier; +import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * An {@link OperationPreprocessor} that modifies URIs in the request and in the response + * by changing one or more of their host, scheme, and port. URIs in the following + * locations are modified: + *
        + *
      • {@link OperationRequest#getUri() Request URI} + *
      • {@link OperationRequest#getHeaders() Request headers} + *
      • {@link OperationRequest#getContent() Request content} + *
      • {@link OperationRequestPart#getHeaders() Request part headers} + *
      • {@link OperationRequestPart#getContent() Request part content} + *
      • {@link OperationResponse#getHeaders() Response headers} + *
      • {@link OperationResponse#getContent() Response content} + *
      + * + * @author Andy Wilkinson + */ +public final class UriModifyingOperationPreprocessor implements OperationPreprocessor { + + private final UriModifyingContentModifier contentModifier = new UriModifyingContentModifier(); + + private final OperationPreprocessor contentModifyingDelegate = new ContentModifyingOperationPreprocessor( + this.contentModifier); + + private String scheme; + + private String host; + + private String port; + + /** + * Modifies the URI to use the given {@code scheme}. {@code null}, the default, will + * leave the scheme unchanged. + * + * @param scheme the scheme + * @return {@code this} + */ + public UriModifyingOperationPreprocessor scheme(String scheme) { + this.scheme = scheme; + this.contentModifier.setScheme(scheme); + return this; + } + + /** + * Modifies the URI to use the given {@code host}. {@code null}, the default, will + * leave the host unchanged. + * + * @param host the host + * @return {@code this} + */ + public UriModifyingOperationPreprocessor host(String host) { + this.host = host; + this.contentModifier.setHost(host); + return this; + } + + /** + * Modifies the URI to use the given {@code port}. + * + * @param port the port + * @return {@code this} + */ + public UriModifyingOperationPreprocessor port(int port) { + return port(Integer.toString(port)); + } + + /** + * Removes the port from the URI. + * + * @return {@code this} + */ + public UriModifyingOperationPreprocessor removePort() { + return port(""); + } + + private UriModifyingOperationPreprocessor port(String port) { + this.port = port; + this.contentModifier.setPort(port); + return this; + } + + @Override + public OperationRequest preprocess(OperationRequest request) { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUri(request.getUri()); + if (this.scheme != null) { + uriBuilder.scheme(this.scheme); + } + if (this.host != null) { + uriBuilder.host(this.host); + } + if (this.port != null) { + if (StringUtils.hasText(this.port)) { + uriBuilder.port(this.port); + } + else { + uriBuilder.port(null); + } + } + HttpHeaders modifiedHeaders = modify(request.getHeaders()); + if (this.host != null) { + modifiedHeaders.set(HttpHeaders.HOST, this.host); + } + return this.contentModifyingDelegate.preprocess(new OperationRequestFactory() + .create(uriBuilder.build().toUri(), request.getMethod(), + request.getContent(), modifiedHeaders, request.getParameters(), + modify(request.getParts()))); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + return this.contentModifyingDelegate.preprocess(new OperationResponseFactory() + .create(response.getStatus(), modify(response.getHeaders()), + response.getContent())); + } + + private HttpHeaders modify(HttpHeaders headers) { + HttpHeaders modified = new HttpHeaders(); + for (Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + modified.add(header.getKey(), this.contentModifier.modify(value)); + } + } + return modified; + } + + private Collection modify(Collection parts) { + List modifiedParts = new ArrayList<>(); + OperationRequestPartFactory factory = new OperationRequestPartFactory(); + for (OperationRequestPart part : parts) { + modifiedParts.add(factory.create(part.getName(), part.getSubmittedFileName(), + this.contentModifier.modifyContent(part.getContent(), part + .getHeaders().getContentType()), modify(part.getHeaders()))); + } + return modifiedParts; + } + + private static final class UriModifyingContentModifier implements ContentModifier { + + private static final Pattern SCHEME_HOST_PORT_PATTERN = Pattern + .compile("(http[s]?)://([^/:#?]+)(:[0-9]+)?"); + + private String scheme; + + private String host; + + private String port; + + private void setScheme(String scheme) { + this.scheme = scheme; + } + + private void setHost(String host) { + this.host = host; + } + + private void setPort(String port) { + this.port = port; + } + + @Override + public byte[] modifyContent(byte[] content, MediaType contentType) { + String input; + if (contentType != null && contentType.getCharSet() != null) { + input = new String(content, contentType.getCharSet()); + } + else { + input = new String(content); + } + + return modify(input).getBytes(); + } + + private String modify(String input) { + List replacements = Arrays.asList(this.scheme, this.host, + StringUtils.hasText(this.port) ? ":" + this.port : this.port); + + int previous = 0; + + Matcher matcher = SCHEME_HOST_PORT_PATTERN.matcher(input); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + if (matcher.start(i) >= 0) { + builder.append(input.substring(previous, matcher.start(i))); + } + if (matcher.start(i) >= 0) { + previous = matcher.end(i); + } + builder.append(getReplacement(matcher.group(i), + replacements.get(i - 1))); + } + } + + if (previous < input.length()) { + builder.append(input.substring(previous)); + } + return builder.toString(); + } + + private String getReplacement(String original, String candidate) { + if (candidate != null) { + return candidate; + } + if (original != null) { + return original; + } + return ""; + } + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java new file mode 100644 index 000000000..83ffc61b7 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * REST Assured-specific support for preprocessing an operation prior to it being + * documented. + */ +package org.springframework.restdocs.restassured.operation.preprocess; + diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java new file mode 100644 index 000000000..a8b893fae --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Core classes for using Spring REST Docs with REST Assured. + */ +package org.springframework.restdocs.restassured; + diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java new file mode 100644 index 000000000..bb5325598 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java @@ -0,0 +1,251 @@ +/* + * Copyright 2012-2016 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.restassured; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.restassured.RestAssuredOperationRequestFactoryTests.TestApplication; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RestAssuredOperationRequestFactory}. + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = TestApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +public class RestAssuredOperationRequestFactoryTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final RestAssuredOperationRequestFactory factory = new RestAssuredOperationRequestFactory(); + + @Value("${local.server.port}") + private int port; + + @Test + public void requestUri() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.get("/foo/bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri(), + is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); + } + + @Test + public void requestMethod() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.head("/foo/bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getMethod(), is(equalTo(HttpMethod.HEAD))); + } + + @Test + public void queryStringParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .queryParam("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void queryStringFromUrlParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.get("/?foo=bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void formParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .formParameter("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void requestParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .parameter("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void headers() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .header("Foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getHeaders().toString(), request.getHeaders().size(), is(3)); + assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); + assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); + assertThat(request.getHeaders().get("Host"), + is(equalTo(Arrays.asList("localhost")))); + } + + @Test + public void multipart() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("a", "a.txt", "alpha", null) + .multiPart("b", new ObjectBody("bar"), "application/json"); + requestSpec.post().then().statusCode(200); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + Collection parts = request.getParts(); + assertThat(parts.size(), is(2)); + Iterator iterator = parts.iterator(); + OperationRequestPart part = iterator.next(); + assertThat(part.getName(), is(equalTo("a"))); + assertThat(part.getSubmittedFileName(), is(equalTo("a.txt"))); + assertThat(part.getContentAsString(), is(equalTo("alpha"))); + assertThat(part.getHeaders().getContentType(), is(equalTo(MediaType.TEXT_PLAIN))); + part = iterator.next(); + assertThat(part.getName(), is(equalTo("b"))); + assertThat(part.getSubmittedFileName(), is(equalTo("file"))); + assertThat(part.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); + assertThat(part.getHeaders().getContentType(), + is(equalTo(MediaType.APPLICATION_JSON))); + } + + @Test + public void byteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) + .port(this.port); + requestSpec.post(); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + @Test + public void stringBody() { + RequestSpecification requestSpec = RestAssured.given().body("body") + .port(this.port); + requestSpec.post(); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("body"))); + } + + @Test + public void objectBody() { + RequestSpecification requestSpec = RestAssured.given() + .body(new ObjectBody("bar")).port(this.port); + requestSpec.post(); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); + } + + @Test + public void inputStreamBodyIsNotSupported() { + RequestSpecification requestSpec = RestAssured.given() + .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) + .port(this.port); + requestSpec.post(); + this.thrown + .expectMessage(equalTo("Unsupported request content: java.io.ByteArrayInputStream")); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + @Test + public void fileBodyIsNotSupported() { + RequestSpecification requestSpec = RestAssured.given() + .body(new File("src/test/resources/body.txt")).port(this.port); + requestSpec.post(); + this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + /** + * Minimal test web application. + */ + @Configuration + @EnableAutoConfiguration + @RestController + static class TestApplication { + + @RequestMapping("/") + void handle() { + } + + } + + /** + * Sample object body to verify JSON serialization. + */ + static class ObjectBody { + + private final String foo; + + ObjectBody(String foo) { + this.foo = foo; + } + + public String getFoo() { + return this.foo; + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java new file mode 100644 index 000000000..1f7bbbd44 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2016 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.restassured; + +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.TemplateEngine; + +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestAssuredRestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class RestAssuredRestDocumentationConfigurerTests { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); + + private final FilterableResponseSpecification responseSpec = mock(FilterableResponseSpecification.class); + + private final FilterContext filterContext = mock(FilterContext.class); + + private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( + this.restDocumentation); + + @Test + public void nextFilterIsCalled() { + this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + verify(this.filterContext).setValue( + eq("org.springframework.restdocs.configuration"), any(Map.class)); + } + + @Test + public void configurationIsAddedToTheContext() { + this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + @SuppressWarnings("rawtypes") + ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); + verify(this.filterContext).setValue( + eq("org.springframework.restdocs.configuration"), + configurationCaptor.capture()); + @SuppressWarnings("unchecked") + Map configuration = configurationCaptor.getValue(); + assertThat( + configuration, + hasEntry(equalTo(TemplateEngine.class.getName()), + instanceOf(TemplateEngine.class))); + assertThat( + configuration, + hasEntry(equalTo(WriterResolver.class.getName()), + instanceOf(WriterResolver.class))); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + } +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java new file mode 100644 index 000000000..3ae6d13f5 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -0,0 +1,374 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.hypermedia.Link; +import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.SnippetMatchers.snippet; + +/** + * Integration tests for using Spring REST Docs with REST Assured. + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = TestApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +public class RestAssuredRestDocumentationIntegrationTests { + + @Rule + public RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + @Value("${local.server.port}") + private int port; + + @Test + public void defaultSnippetGeneration() { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("default")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void curlSnippetWithContent() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-content")).accept("application/json") + .content("content").contentType(contentType).post("/").then() + .statusCode(200); + + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl 'http://localhost:" + this.port + "/' -i " + + "-X POST -H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "-d 'content'")))); + } + + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-query-string")) + .accept("application/json").param("foo", "bar").param("a", "alpha") + .post("/?foo=bar").then().statusCode(200); + String contentType = "application/x-www-form-urlencoded; charset=ISO-8859-1"; + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + "'http://localhost:" + this.port + + "/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "-d 'a=alpha'")))); + } + + @Test + public void linksSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("links", + links(linkWithRel("rel").description("The description")))) + .accept("application/json").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "links.adoc"); + } + + @Test + public void pathParametersSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "path-parameters", + pathParameters(parameterWithName("foo").description( + "The description")))).accept("application/json") + .get("/{foo}", "").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/path-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); + } + + @Test + public void requestParametersSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "request-parameters", + requestParameters(parameterWithName("foo").description( + "The description")))).accept("application/json") + .param("foo", "bar").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/request-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); + } + + @Test + public void requestFieldsSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-fields", requestFields(fieldWithPath("a") + .description("The description")))).accept("application/json") + .content("{\"a\":\"alpha\"}").post("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/request-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); + } + + @Test + public void responseFieldsSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "response-fields", + responseFields( + fieldWithPath("a").description("The description"), + fieldWithPath("links").description( + "Links to other resources")))) + .accept("application/json").get("/").then().statusCode(200); + + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/response-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); + } + + @Test + public void parameterizedOutputDirectory() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("{method-name}")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/parameterized-output-directory"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void multiStep() throws Exception { + RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}-{step}")).build(); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void preprocessedRequest() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .header("a", "alpha") + .header("b", "bravo") + .contentType("application/json") + .accept("application/json") + .content("{\"a\":\"alpha\"}") + .filter(document("original-request")) + .filter(document( + "preprocessed-request", + preprocessRequest( + prettyPrint(), + removeHeaders("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH), + replacePattern(pattern, "\"<>\"")))).get("/") + .then().statusCode(200); + assertThat( + new File("build/generated-snippets/original-request/http-request.adoc"), + is(snippet() + .withContents( + httpRequest(RequestMethod.GET, "/") + .header("a", "alpha") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", + "application/json; charset=UTF-8") + .header("Host", "localhost") + .header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat( + new File( + "build/generated-snippets/preprocessed-request/http-request.adoc"), + is(snippet() + .withContents( + httpRequest(RequestMethod.GET, "/") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", + "application/json; charset=UTF-8") + .content(prettyPrinted)))); + } + + @Test + public void preprocessedResponse() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("original-response")) + .filter(document( + "preprocessed-response", + preprocessResponse( + prettyPrint(), + maskLinks(), + removeHeaders("a", "Transfer-Encoding", "Date", "Server"), + replacePattern(pattern, "\"<>\""), modifyUris() + .scheme("https").host("api.example.com") + .removePort()))).get("/").then().statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat( + new File( + "build/generated-snippets/preprocessed-response/http-response.adoc"), + is(snippet().withContents( + httpResponse(HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); + } + + @Test + public void customSnippetTemplate() throws Exception { + ClassLoader classLoader = new URLClassLoader(new URL[] { new File( + "src/test/resources/custom-snippet-templates").toURI().toURL() }, + getClass().getClassLoader()); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + try { + given().port(this.port).accept("application/json") + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("custom-snippet-template")).get("/").then() + .statusCode(200); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + assertThat(new File( + "build/generated-snippets/custom-snippet-template/curl-request.adoc"), + is(snippet().withContents(equalTo("Custom curl request")))); + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + for (String snippet : snippets) { + File snippetFile = new File(directory, snippet); + assertTrue("Snippet " + snippetFile + " not found", snippetFile.isFile()); + } + } + + /** + * Minimal test application called by the tests. + */ + @Configuration + @EnableAutoConfiguration + @RestController + static class TestApplication { + + @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> foo() { + Map response = new HashMap<>(); + response.put("a", "alpha"); + response.put("links", Arrays.asList(new Link("rel", "href"))); + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "alpha"); + headers.add("Foo", "http://localhost:12345/foo/bar"); + return new ResponseEntity<>(response, headers, HttpStatus.OK); + } + + @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) + public String bar() { + return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..a962c782a --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -0,0 +1,343 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link UriModifyingOperationPreprocessor}. + * + * @author Andy Wilkinson + */ +public class UriModifyingOperationPreprocessorTests { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); + + @Test + public void requestUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://localhost:12345")); + assertThat(processed.getUri(), is(equalTo(URI.create("https://localhost:12345")))); + } + + @Test + public void requestUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com:12345")))); + } + + @Test + public void requestUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com:23456")))); + } + + @Test + public void requestUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com")))); + } + + @Test + public void requestUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345/foo/bar")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com/foo/bar")))); + } + + @Test + public void requestUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345?foo=bar")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com?foo=bar")))); + } + + @Test + public void requestUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345#foo")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com#foo")))); + } + + @Test + public void requestContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'https://localhost:12345' should be used"))); + } + + @Test + public void requestContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + @Test + public void requestContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost:23456' should be used"))); + } + + @Test + public void requestContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost' should be used"))); + } + + @Test + public void multipleRequestContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat( + new String(processed.getContent()), + is(equalTo("Use 'http://localhost' or 'https://localhost' to access the service"))); + } + + @Test + public void requestContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); + } + + @Test + public void requestContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); + } + + @Test + public void requestContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost#foo' should be used"))); + } + + @Test + public void responseContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'https://localhost:12345' should be used"))); + } + + @Test + public void responseContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + @Test + public void responseContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost:23456' should be used"))); + } + + @Test + public void responseContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost' should be used"))); + } + + @Test + public void multipleResponseContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat( + new String(processed.getContent()), + is(equalTo("Use 'http://localhost' or 'https://localhost' to access the service"))); + } + + @Test + public void responseContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); + } + + @Test + public void responseContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); + } + + @Test + public void responseContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost#foo' should be used"))); + } + + @Test + public void urisInRequestHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess(createRequestWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + assertThat(processed.getHeaders().getFirst("Host"), + is(equalTo("api.example.com"))); + } + + @Test + public void urisInResponseHeadersCanBeModified() { + OperationResponse processed = this.preprocessor.host("api.example.com") + .preprocess(createResponseWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + } + + @Test + public void urisInRequestPartHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess( + createRequestWithPartWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + } + + @Test + public void urisInRequestPartContentCanBeModified() { + OperationRequest processed = this.preprocessor + .host("api.example.com") + .preprocess( + createRequestWithPartWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getParts().iterator().next().getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + private OperationRequest createRequestWithUri(String uri) { + return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], + new HttpHeaders(), new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + content.getBytes(), new HttpHeaders(), new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], headers, new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithPartWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], new HttpHeaders(), new Parameters(), Arrays + .asList(new OperationRequestPartFactory().create("part", + "fileName", new byte[0], headers))); + } + + private OperationRequest createRequestWithPartWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], new HttpHeaders(), new Parameters(), Arrays + .asList(new OperationRequestPartFactory().create("part", + "fileName", content.getBytes(), new HttpHeaders()))); + } + + private OperationResponse createResponseWithContent(String content) { + return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), + content.getBytes()); + } + + private OperationResponse createResponseWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); + } + +} diff --git a/spring-restdocs-restassured/src/test/resources/body.txt b/spring-restdocs-restassured/src/test/resources/body.txt new file mode 100644 index 000000000..1a010b1c0 --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/body.txt @@ -0,0 +1 @@ +file \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet new file mode 100644 index 000000000..07f3a48ff --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet @@ -0,0 +1 @@ +Custom curl request \ No newline at end of file From 5dbb31f38a1d0bf3f1e93c2c57827c72d79ede6f Mon Sep 17 00:00:00 2001 From: mnhock Date: Fri, 22 Jan 2016 16:55:03 +0100 Subject: [PATCH 0207/1059] Fix String comparison in HttpRequestSnippet Closes gh-186 --- .../org/springframework/restdocs/http/HttpRequestSnippet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 908b2fb84..26d209fab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -82,7 +82,7 @@ private List> getHeaders(OperationRequest request) { for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { - if (header.getKey() == HttpHeaders.CONTENT_TYPE + if (HttpHeaders.CONTENT_TYPE.equals(header.getKey()) && !request.getParts().isEmpty()) { headers.add(header(header.getKey(), String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); From 91d6e2f8ec7c52671cf76941e4881f1aecdaad49 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 1 Feb 2016 12:37:23 +0000 Subject: [PATCH 0208/1059] Improve extensibility and reusability of MustacheTemplateEngine Previously, MustacheTemplateEngine hard coded the configuration of its Mustache Compiler. This commit adds an overloaded constructor that allows a custom Compiler to be used. It also adds protected accessors for the compiler and the template resource resolver so that they can be easily accessed by subclasses. Closes gh-149 --- .../mustache/MustacheTemplateEngine.java | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index df142045a..9e372ac4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -36,18 +36,32 @@ */ public class MustacheTemplateEngine implements TemplateEngine { - private final Compiler compiler = Mustache.compiler().escapeHTML(false); + private final Compiler compiler; private final TemplateResourceResolver templateResourceResolver; /** - * Creates a new {@link MustacheTemplateEngine} that will use the given + * Creates a new {@code MustacheTemplateEngine} that will use the given * {@code templateResourceResolver} to resolve template paths. * - * @param templateResourceResolver The resolve to use + * @param templateResourceResolver the resolver to use */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { + this(templateResourceResolver, Mustache.compiler().escapeHTML(false)); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code tempalteResourceResolver} to resolve templates and the given + * {@code compiler} to compile them. + * + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + Compiler compiler) { this.templateResourceResolver = templateResourceResolver; + this.compiler = compiler; } @Override @@ -58,4 +72,23 @@ public Template compileTemplate(String name) throws IOException { templateResource.getInputStream()))); } + /** + * Returns the {@link Compiler} used to compile Mustache templates. + * + * @return the compiler + */ + protected final Compiler getCompiler() { + return this.compiler; + } + + /** + * Returns the {@link TemplateResourceResolver} used to resolve the template resources + * prior to compilation. + * + * @return the resolver + */ + protected final TemplateResourceResolver getTemplateResourceResolver() { + return this.templateResourceResolver; + } + } From 29675728aa20cf36e6674a89708c502a3e324d07 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Feb 2016 09:32:17 +0000 Subject: [PATCH 0209/1059] Introduce a code of conduct --- CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 15 +++++++++++---- README.md | 12 +++++++----- 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..4df5b8d41 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open +and welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or +patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors +that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at spring-code-of-conduct@pivotal.io. All complaints will +be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. Maintainers are obligated to maintain confidentiality +with regard to the reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][1], version 1.3.0, available +at [contributor-covenant.org/version/1/3/0/][2]. + +[1]: http://contributor-covenant.org +[2]: http://contributor-covenant.org/version/1/3/0/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d58b56368..5b609d53c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,10 +4,16 @@ Spring REST Docs is released under the Apache 2.0 license. If you would like to contribute something, or simply want to work with the code, this document should help you to get started. +## Code of conduct + +This project adheres to the Contributor Covenant [code of conduct][1]. By participating, +you are expected to uphold this code. Please report unacceptable behavior to +spring-code-of-conduct@pivotal.io. + ## Sign the contributor license agreement Before we accept a non-trivial patch or pull request we will need you to sign the -[contributor's license agreement (CLA)][1]. Signing the contributor's agreement does not +[contributor's license agreement (CLA)][2]. Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Please use "Andy Wilkinson" in the project lead field when you complete the form. @@ -28,7 +34,7 @@ None of these is essential for a pull request, but they will all help - Add unit tests that covers and new or modified functionality - Whenever possible, please rebase your branch against the current master (or other target branch in the main project). -* When writing a commit message please follow [these conventions][2]. Also, if you are +* When writing a commit message please follow [these conventions][3]. Also, if you are fixing an existing issue please add `Fixes gh-nnn` at the end of the commit message (where nnn is the issue number). @@ -54,5 +60,6 @@ $ ./gradlew eclipse The project can then be imported into Eclipse using `File -> Import…` and then selecting `General -> Existing Projects into Workspace`. -[1]: https://support.springsource.com/spring_committer_signup -[2]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file +[1]: CODE_OF_CONDUCT.md +[2]: https://support.springsource.com/spring_committer_signup +[3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file diff --git a/README.md b/README.md index 8ca99f410..aaa0c6dcc 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,12 @@ Spring REST Docs requires Java 7 or later and is built using [Gradle][10]: ## Contributing -[Pull requests][11] are welcome. Please see the [contributor guidelines][12] for details. +Contributors to this project agree to uphold its [code of conduct][11]. +[Pull requests][12] are welcome. Please see the [contributor guidelines][13] for details. ## Licence -Spring REST Docs is open source software released under the [Apache 2.0 license][13]. +Spring REST Docs is open source software released under the [Apache 2.0 license][14]. [1]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) [2]: https://build.spring.io/browse/SRD-PUB @@ -41,7 +42,8 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [8]: https://www.youtube.com/watch?v=knH5ihPNiUs&feature=youtu.be [9]: http://docs.spring.io/spring-restdocs/docs/ [10]: http://gradle.org -[11]: https://help.github.com/articles/using-pull-requests/ -[12]: https://github.com/spring-projects/spring-restdocs/blob/master/CONTRIBUTING.md -[13]: http://www.apache.org/licenses/LICENSE-2.0.html +[11]: CODE_OF_CONDUCT.md +[12]: https://help.github.com/articles/using-pull-requests/ +[13]: CONTRIBUTING.md +[14]: http://www.apache.org/licenses/LICENSE-2.0.html From 4d37dea1d6d8ef21acea5043b4280799e7110207 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jan 2016 16:10:00 +0000 Subject: [PATCH 0210/1059] Add support for generating snippets in Markdown This commit introduces support for generating snippets formatted using Markdown. Asciidoctor remains the default. A new SnippetFormat abstraction has been introduced with Asciidoctor and Markdown implementations provided out of the box. Markdown-formatted templates are also provided for all of the default snippets. Please refer to the updated reference documentation for further details. Closes gh-150 Closes gh-19 --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 23 +- docs/src/docs/asciidoc/index.adoc | 1 + docs/src/docs/asciidoc/introduction.adoc | 5 +- .../docs/asciidoc/working-with-markdown.adoc | 28 +++ .../com/example/mockmvc/CustomFormat.java | 50 ++++ .../com/example/restassured/CustomFormat.java | 46 ++++ .../config/RestDocumentationConfigurer.java | 19 +- .../restdocs/config/SnippetConfiguration.java | 45 ++++ .../restdocs/config/SnippetConfigurer.java | 30 ++- .../restdocs/snippet/SnippetFormat.java | 34 +++ .../restdocs/snippet/SnippetFormats.java | 74 ++++++ .../snippet/StandardWriterResolver.java | 36 ++- .../restdocs/snippet/WriterResolver.java | 5 +- .../StandardTemplateResourceResolver.java | 39 +++- .../curl-request.snippet} | 0 .../http-request.snippet} | 0 .../http-response.snippet} | 0 .../links.snippet} | 0 .../path-parameters.snippet} | 0 .../request-fields.snippet} | 0 .../request-headers.snippet} | 0 .../request-parameters.snippet} | 0 .../response-fields.snippet} | 0 .../response-headers.snippet} | 0 .../templates/md/curl-request.snippet | 3 + .../templates/md/http-request.snippet | 7 + .../templates/md/http-response.snippet | 7 + .../restdocs/templates/md/links.snippet | 5 + .../templates/md/path-parameters.snippet | 6 + .../templates/md/request-fields.snippet | 5 + .../templates/md/request-headers.snippet | 5 + .../templates/md/request-parameters.snippet | 5 + .../templates/md/response-fields.snippet | 5 + .../templates/md/response-headers.snippet | 5 + .../restdocs/AbstractSnippetTests.java | 96 ++++++++ .../curl/CurlRequestSnippetTests.java | 145 +++++------- .../RequestHeadersSnippetFailureTests.java | 72 ++++++ .../headers/RequestHeadersSnippetTests.java | 113 +++------ .../ResponseHeadersSnippetFailureTests.java | 73 ++++++ .../headers/ResponseHeadersSnippetTests.java | 114 +++------ .../http/HttpRequestSnippetTests.java | 64 ++---- .../http/HttpResponseSnippetTests.java | 39 ++-- .../hypermedia/LinksSnippetFailureTests.java | 79 +++++++ .../hypermedia/LinksSnippetTests.java | 141 +++--------- .../hypermedia/StubLinkExtractor.java | 47 ++++ .../AsciidoctorRequestFieldsSnippetTests.java | 74 ++++++ .../RequestFieldsSnippetFailureTests.java | 162 +++++++++++++ .../payload/RequestFieldsSnippetTests.java | 217 +++--------------- .../ResponseFieldsSnippetFailureTests.java | 143 ++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 207 ++++------------- .../PathParametersSnippetFailureTests.java | 85 +++++++ .../request/PathParametersSnippetTests.java | 127 +++------- .../RequestParametersSnippetFailureTests.java | 84 +++++++ .../RequestParametersSnippetTests.java | 104 ++------- .../snippet/StandardWriterResolverTests.java | 5 +- ...StandardTemplateResourceResolverTests.java | 8 +- .../restdocs/test/ExpectedSnippet.java | 15 +- .../restdocs/test/OperationBuilder.java | 17 +- .../restdocs/test/SnippetMatchers.java | 212 ++++++++++++++--- .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../{ => adoc}/links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 ...quest-fields-with-list-description.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 .../md/curl-request-with-title.snippet | 4 + .../md/http-request-with-title.snippet | 8 + .../md/http-response-with-title.snippet | 8 + .../md/links-with-extra-column.snippet | 5 + .../md/links-with-title.snippet | 6 + .../path-parameters-with-extra-column.snippet | 5 + .../md/path-parameters-with-title.snippet | 6 + .../request-fields-with-extra-column.snippet | 5 + .../md/request-fields-with-title.snippet | 6 + .../request-headers-with-extra-column.snippet | 5 + .../md/request-headers-with-title.snippet | 6 + ...quest-parameters-with-extra-column.snippet | 5 + .../md/request-parameters-with-title.snippet | 6 + .../response-fields-with-extra-column.snippet | 5 + .../md/response-fields-with-title.snippet | 6 + ...response-headers-with-extra-column.snippet | 5 + .../md/response-headers-with-title.snippet | 6 + .../MockMvcRestDocumentationConfigurer.java | 4 +- ...kMvcRestDocumentationIntegrationTests.java | 54 +++-- ...estAssuredRestDocumentationConfigurer.java | 4 +- ...uredRestDocumentationIntegrationTests.java | 23 +- 99 files changed, 2089 insertions(+), 1026 deletions(-) create mode 100644 docs/src/docs/asciidoc/working-with-markdown.adoc create mode 100644 docs/src/test/java/com/example/mockmvc/CustomFormat.java create mode 100644 docs/src/test/java/com/example/restassured/CustomFormat.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-curl-request.snippet => adoc/curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-http-request.snippet => adoc/http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-http-response.snippet => adoc/http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-links.snippet => adoc/links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-path-parameters.snippet => adoc/path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-fields.snippet => adoc/request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-headers.snippet => adoc/request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-parameters.snippet => adoc/request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-response-fields.snippet => adoc/response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-response-headers.snippet => adoc/response-headers.snippet} (100%) create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-list-description.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-headers-with-title.snippet (100%) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index ebc91d6ff..4c188cd45 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.snippet.SnippetFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 239588003..f068507e9 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -42,8 +42,7 @@ TIP: To configure a request's context path, use the `contextPath` method on [[configuration-snippet-encoding]] === Snippet encoding -The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same -default for the snippets that it generates. You can change the default snippet encoding +The default snippet encoding is `UTF-8`. You can change the default snippet encoding using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: [source,java,indent=0,role="primary"] @@ -60,6 +59,26 @@ include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom- +[[configuration-snippet-format]] +=== Snippet format + +The default snippet format is Asciidoctor. Markdown is also supported out of the box. You +can change the default format using the `RestDocumentationConfigurer` API: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-format] +---- + + + [[configuration-default-snippets]] === Default snippets diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 17cde86ea..10777c6af 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -26,4 +26,5 @@ include::documenting-your-api.adoc[] include::customizing-requests-and-responses.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] +include::working-with-markdown.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 3bfaef7d3..7a6fde223 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -6,8 +6,9 @@ services that is accurate and readable. Writing high-quality documentation is difficult. One way to ease that difficulty is to use tools that are well-suited to the job. To this end, Spring REST Docs uses -http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produces -HTML, styled and layed out to suit your needs. +http://asciidoctor.org[Asciidoctor] by default. Asciidoctor processes plain text and +produces HTML, styled and layed out to suit your needs. If you prefer, Spring REST Docs +can also be configured to use Markdown. Spring REST Docs makes use of snippets produced by tests written with {spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or diff --git a/docs/src/docs/asciidoc/working-with-markdown.adoc b/docs/src/docs/asciidoc/working-with-markdown.adoc new file mode 100644 index 000000000..6f6d8ed78 --- /dev/null +++ b/docs/src/docs/asciidoc/working-with-markdown.adoc @@ -0,0 +1,28 @@ +[[working-with-markdown]] +== Working with Markdown + +This section describes any aspects of working with Markdown that are particularly +relevant to Spring REST Docs. + + + +[[working-with-markdown-limitations]] +=== Limitations + +Markdown was originally designed for people writing for the web and, as such, isn't +as well-suited to writing documentation as Asciidoctor. Typically, these limitations +are overcome by using another tool that builds on top of Markdown. + +Markdown has no official support for tables. Spring REST Docs' default Markdown snippet +templates use https://michelf.ca/projects/php-markdown/extra/#table[Markdown Extra's table +format]. + + + +[[working-with-markdown-including-snippets]] +=== Including snippets + +Markdown has no built-in support for including one Markdown file in another. To include +the generated snippets of Markdown in your documentation, you should use an additional +tool that supports this functionality. One example that's particularly well-suited to +documenting APIs is https://github.com/tripit/slate[Slate]. \ No newline at end of file diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java new file mode 100644 index 000000000..ab121224a --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class CustomFormat { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + // tag::custom-format[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withFormat(SnippetFormats.markdown())) + .build(); + // end::custom-format[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java new file mode 100644 index 000000000..649becb25 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.SnippetFormats; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomFormat { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-format[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withFormat(SnippetFormats.markdown())) + .build(); + // end::custom-format[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index bedec85ae..0f0ff9b21 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -93,13 +93,20 @@ protected final AbstractConfigurer getTemplateEngineConfigurer() { private static final class TemplateEngineConfigurer extends AbstractConfigurer { - private TemplateEngine templateEngine = new MustacheTemplateEngine( - new StandardTemplateResourceResolver()); + private TemplateEngine templateEngine; @Override public void apply(Map configuration, RestDocumentationContext context) { - configuration.put(TemplateEngine.class.getName(), this.templateEngine); + TemplateEngine engineToUse = this.templateEngine; + if (engineToUse == null) { + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + engineToUse = new MustacheTemplateEngine( + new StandardTemplateResourceResolver( + snippetConfiguration.getFormat())); + } + configuration.put(TemplateEngine.class.getName(), engineToUse); } private void setTemplateEngine(TemplateEngine templateEngine) { @@ -117,8 +124,12 @@ public void apply(Map configuration, RestDocumentationContext context) { WriterResolver resolverToUse = this.writerResolver; if (resolverToUse == null) { + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context)); + new RestDocumentationContextPlaceholderResolver(context), + snippetConfiguration.getEncoding(), + snippetConfiguration.getFormat()); } configuration.put(WriterResolver.class.getName(), resolverToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java new file mode 100644 index 000000000..ecfd94e20 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.config; + +import org.springframework.restdocs.snippet.SnippetFormat; + +/** + * An encapsulation of the configuration for documentation snippets. + * + * @author Andy Wilkinson + */ +class SnippetConfiguration { + + private final String encoding; + + private final SnippetFormat format; + + SnippetConfiguration(String encoding, SnippetFormat format) { + this.encoding = encoding; + this.format = format; + } + + String getEncoding() { + return this.encoding; + } + + SnippetFormat getFormat() { + return this.format; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 6f4be5a54..a028a7c6f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -24,7 +24,8 @@ import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; /** * A configurer that can be used to configure the generated documentation snippets. @@ -51,8 +52,18 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

      configuration, RestDocumentationContext context) { - ((WriterResolver) configuration.get(WriterResolver.class.getName())) - .setEncoding(this.snippetEncoding); + configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( + this.snippetEncoding, this.snippetFormat)); configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); } @@ -93,4 +104,17 @@ public T withDefaults(Snippet... defaultSnippets) { this.defaultSnippets = Arrays.asList(defaultSnippets); return (T) this; } + + /** + * Configures the format of the documentation snippets. + * + * @param format the snippet format + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public T withFormat(SnippetFormat format) { + this.snippetFormat = format; + return (T) this; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java new file mode 100644 index 000000000..58e31f04e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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.snippet; + +/** + * A {@link SnippetFormat} provides information about a particular snippet format, such as + * Asciidoctor or Markdown. + * + * @author Andy Wilkinson + */ +public interface SnippetFormat { + + /** + * Returns the snippet format's file extension. + * + * @return the file extension + */ + String getFileExtension(); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java new file mode 100644 index 000000000..8c24861a4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2016 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.snippet; + +/** + * An enumeration of the built-in snippet formats. + * + * @author Andy Wilkinson + */ +public abstract class SnippetFormats { + + private static final SnippetFormat ASCIIDOCTOR = new AsciidoctorSnippetFormat(); + + private static final SnippetFormat MARKDOWN = new MarkdownSnippetFormat(); + + private SnippetFormats() { + + } + + /** + * Returns the Asciidoctor snippet format. + * + * @return the snippet format + */ + public static SnippetFormat asciidoctor() { + return ASCIIDOCTOR; + } + + /** + * Returns the Markdown snippet format. + * + * @return the snippet format + */ + public static SnippetFormat markdown() { + return MARKDOWN; + } + + private static final class AsciidoctorSnippetFormat implements SnippetFormat { + + private static final String FILE_EXTENSION = "adoc"; + + @Override + public String getFileExtension() { + return FILE_EXTENSION; + } + + } + + private static final class MarkdownSnippetFormat implements SnippetFormat { + + private static final String FILE_EXTENSION = "md"; + + @Override + public String getFileExtension() { + return FILE_EXTENSION; + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1ef51ea4e..1538da24b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -33,29 +33,54 @@ */ public final class StandardWriterResolver implements WriterResolver { - private String encoding = "UTF-8"; - private final PlaceholderResolver placeholderResolver; private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "{", "}"); + private String encoding = "UTF-8"; + + private SnippetFormat snippetFormat; + /** * Creates a new {@code StandardWriterResolver} that will use the given * {@code placeholderResolver} to resolve any placeholders in the - * {@code operationName}. + * {@code operationName}. Writers will use {@code UTF-8} encoding and, when writing to + * a file, will use a filename appropriate for Asciidoctor content. * * @param placeholderResolver the placeholder resolver + * @deprecated since 1.1.0 in favor of + * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, SnippetFormat)} */ + @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { + this(placeholderResolver, "UTF-8", SnippetFormats.asciidoctor()); + } + + /** + * Creates a new {@code StandardWriterResolver} that will use the given + * {@code placeholderResolver} to resolve any placeholders in the + * {@code operationName}. Writers will use the given {@code encoding} and, when + * writing to a file, will use a filename appropriate for content in the given + * {@code snippetFormat}. + * + * @param placeholderResolver the placeholder resolver + * @param encoding the encoding + * @param snippetFormat the snippet format + */ + public StandardWriterResolver(PlaceholderResolver placeholderResolver, + String encoding, SnippetFormat snippetFormat) { this.placeholderResolver = placeholderResolver; + this.encoding = encoding; + this.snippetFormat = snippetFormat; } @Override public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( - operationName, this.placeholderResolver), snippetName + ".adoc", context); + operationName, this.placeholderResolver), snippetName + "." + + this.snippetFormat.getFileExtension(), context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); @@ -95,4 +120,5 @@ private void createDirectoriesIfNecessary(File outputFile) { throw new IllegalStateException("Failed to create directory '" + parent + "'"); } } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java index 29c2393e3..3caac5ac9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -47,7 +47,10 @@ Writer resolve(String operationName, String snippetName, * resolver. * * @param encoding the encoding + * @deprecated since 1.1.0 in favour of configuring the encoding when to resolver is + * created */ + @Deprecated void setEncoding(String encoding); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 4e3d38e30..b8b2c62e8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,27 +18,54 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; /** * Standard implementation of {@link TemplateResourceResolver}. *

      - * Templates are resolved by first looking for a resource on the classpath named + * Templates are resolved by looking for a resource on the classpath named * {@code org/springframework/restdocs/templates/{name}.snippet}. If no such - * resource exists {@code default-} is prepended to the name and the classpath is checked - * again. The built-in snippet templates are all named {@code default- name}, thereby - * allowing them to be overridden. + * resource exists an attempt is made to return a default resource that is appropriate for + * the configured snippet format. * * @author Andy Wilkinson */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { + private final SnippetFormat snippetFormat; + + /** + * Creates a new {@code StandardTemplateResourceResolver} that will produce default + * template resources formatted with Asciidoctor. + * + * @deprecated since 1.1.0 in favour of + * {@link #StandardTemplateResourceResolver(SnippetFormat)} + */ + @Deprecated + public StandardTemplateResourceResolver() { + this(SnippetFormats.asciidoctor()); + } + + /** + * Creates a new {@code StandardTemplateResourceResolver} that will produce default + * template resources formatted with the given {@code snippetFormat}. + * + * @param snippetFormat the format for the default snippet templates + */ + public StandardTemplateResourceResolver(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + @Override public Resource resolveTemplateResource(String name) { ClassPathResource classPathResource = new ClassPathResource( "org/springframework/restdocs/templates/" + name + ".snippet"); if (!classPathResource.exists()) { classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/default-" + name + ".snippet"); + "org/springframework/restdocs/templates/" + + this.snippetFormat.getFileExtension() + "/" + name + + ".snippet"); if (!classPathResource.exists()) { throw new IllegalStateException("Template named '" + name + "' could not be resolved"); diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet new file mode 100644 index 000000000..085894270 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet @@ -0,0 +1,3 @@ +```bash +$ curl {{url}} {{options}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet new file mode 100644 index 000000000..9613f5fc7 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet @@ -0,0 +1,7 @@ +```http +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet new file mode 100644 index 000000000..1b01aa5b7 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet @@ -0,0 +1,7 @@ +```http +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet new file mode 100644 index 000000000..116d8eabe --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet @@ -0,0 +1,5 @@ +Relation | Description +-------- | ----------- +{{#links}} +{{rel}} | {{description}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet new file mode 100644 index 000000000..f5f1daaee --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet @@ -0,0 +1,6 @@ +{{path}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet new file mode 100644 index 000000000..e8d2957f3 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet new file mode 100644 index 000000000..f8c5ed27e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet new file mode 100644 index 000000000..09a8a1992 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet new file mode 100644 index 000000000..e8d2957f3 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet new file mode 100644 index 000000000..f8c5ed27e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java new file mode 100644 index 000000000..83017f29d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2016 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; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; +import org.springframework.restdocs.test.SnippetMatchers; +import org.springframework.restdocs.test.SnippetMatchers.CodeBlockMatcher; +import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; +import org.springframework.restdocs.test.SnippetMatchers.HttpResponseMatcher; +import org.springframework.restdocs.test.SnippetMatchers.TableMatcher; +import org.springframework.web.bind.annotation.RequestMethod; + +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.snippet.SnippetFormats.markdown; + +/** + * Abstract base class for testing snippet generation. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public abstract class AbstractSnippetTests { + + protected final SnippetFormat snippetFormat; + + @Rule + public ExpectedSnippet snippet; + + @Parameters(name = "{0}") + public static List parameters() { + return Arrays.asList(new Object[] { "Asciidoctor", asciidoctor() }, new Object[] { + "Markdown", markdown() }); + } + + public AbstractSnippetTests(String name, SnippetFormat snippetFormat) { + this.snippet = new ExpectedSnippet(snippetFormat); + this.snippetFormat = snippetFormat; + } + + public CodeBlockMatcher codeBlock(String language) { + return SnippetMatchers.codeBlock(this.snippetFormat, language); + } + + public TableMatcher tableWithHeader(String... headers) { + return SnippetMatchers.tableWithHeader(this.snippetFormat, headers); + } + + public TableMatcher tableWithTitleAndHeader(String title, String... headers) { + return SnippetMatchers + .tableWithTitleAndHeader(this.snippetFormat, title, headers); + } + + public HttpRequestMatcher httpRequest(RequestMethod method, String uri) { + return SnippetMatchers.httpRequest(this.snippetFormat, method, uri); + } + + public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { + return SnippetMatchers.httpResponse(this.snippetFormat, responseStatus); + } + + public OperationBuilder operationBuilder(String name) { + return new OperationBuilder(name, this.snippet.getOutputDirectory(), + this.snippetFormat); + } + + protected FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + this.snippetFormat.getFileExtension() + "/" + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 21c1973d6..f93b04674 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -18,17 +18,16 @@ import java.io.IOException; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; import org.springframework.util.Base64Utils; import static org.hamcrest.CoreMatchers.containsString; @@ -36,7 +35,6 @@ import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; /** * Tests for {@link CurlRequestSnippet}. @@ -47,30 +45,27 @@ * @author Jonathan Pearlin * @author Paul-Christian Volkmer */ -public class CurlRequestSnippetTests { +@RunWith(Parameterized.class) +public class CurlRequestSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); + public CurlRequestSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .build()); + new CurlRequestSnippet().document(operationBuilder("get-request").request( + "http://localhost/foo").build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder("non-get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").build()); + new CurlRequestSnippet().document(operationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); } @Test @@ -78,9 +73,8 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-content", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .content("content").build()); + new CurlRequestSnippet().document(operationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); } @Test @@ -89,8 +83,7 @@ public void getRequestWithQueryString() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(new OperationBuilder( - "request-with-query-string", this.snippet.getOutputDirectory()).request( + new CurlRequestSnippet().document(operationBuilder("request-with-query-string").request( "http://localhost/foo?param=value").build()); } @@ -99,8 +92,7 @@ public void postRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("post-request-with-query-string") .request("http://localhost/foo?param=value").method("POST").build()); } @@ -110,8 +102,7 @@ public void postRequestWithOneParameter() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() - .document(new OperationBuilder("post-request-with-one-parameter", - this.snippet.getOutputDirectory()) + .document(operationBuilder("post-request-with-one-parameter") .request("http://localhost/foo").method("POST").param("k1", "v1") .build()); } @@ -123,10 +114,10 @@ public void postRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-multiple-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet() + .document(operationBuilder("post-request-with-multiple-parameters") + .request("http://localhost/foo").method("POST") + .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test @@ -136,10 +127,10 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); } @Test @@ -150,10 +141,10 @@ public void postRequestWithQueryStringAndParameter() throws IOException { codeBlock("bash") .content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string-and-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-query-string-and-parameter") + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); } @Test @@ -165,10 +156,10 @@ public void postRequestWithOverlappingQueryStringAndParameters() throws IOExcept codeBlock("bash") .content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-overlapping-query-string-and-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").build()); } @Test @@ -176,8 +167,7 @@ public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-one-parameter", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("put-request-with-one-parameter") .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @@ -188,11 +178,9 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(new OperationBuilder("put-request-with-multiple-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet().document(operationBuilder("put-request-with-multiple-parameters") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test @@ -201,9 +189,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") + new CurlRequestSnippet().document(operationBuilder( + "put-request-with-url-encoded-parameter").request("http://localhost/foo") .method("PUT").param("k1", "a&b").build()); } @@ -213,8 +200,8 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()).request("http://localhost/foo") + new CurlRequestSnippet().document(operationBuilder("request-with-headers") + .request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -226,8 +213,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-no-original-filename", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-no-original-filename") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); @@ -240,8 +226,7 @@ public void multipartPostWithContentType() throws IOException { + "'image=@documents/images/example.png;type=image/png'"; this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-with-content-type") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -256,9 +241,8 @@ public void multipartPost() throws IOException { + "'image=@documents/images/example.png'"; this.snippet.expectCurlRequest("multipart-post").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") + new CurlRequestSnippet().document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) .submittedFileName("documents/images/example.png").build()); @@ -272,8 +256,7 @@ public void multipartPostWithParameters() throws IOException { + "-F 'b=banana'"; this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-with-parameters") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -281,34 +264,30 @@ public void multipartPostWithParameters() throws IOException { .param("a", "apple", "avocado").param("b", "banana").build()); } - @Test - public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes").withContents( - containsString("curl request title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("curl-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); - new CurlRequestSnippet(attributes(key("title").value("curl request title"))) - .document(new OperationBuilder("custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); - } - @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { this.snippet.expectCurlRequest("basic-auth").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet().document(new OperationBuilder("basic-auth", this.snippet - .getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("basic-auth") .request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) .build()); } + @Test + public void customAttributes() throws IOException { + this.snippet.expectCurlRequest("custom-attributes").withContents( + containsString("curl request title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("curl-request")).willReturn( + snippetResource("curl-request-with-title")); + new CurlRequestSnippet(attributes(key("title").value("curl request title"))) + .document(operationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java new file mode 100644 index 000000000..ca011d2b3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2016 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; + +/** + * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or + * undocumented headers. + * + * @author Andy Wilkinson + */ +public class RequestHeadersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", this.snippet + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index b5b2d1e4c..ed076d263 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,26 +19,20 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link RequestHeadersSnippet}. @@ -46,13 +40,11 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class RequestHeadersSnippetTests { +public class RequestHeadersSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public RequestHeadersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void requestWithHeaders() throws IOException { @@ -67,14 +59,13 @@ public void requestWithHeaders() throws IOException { .description("three"), headerWithName("Accept-Language") .description("four"), headerWithName("Cache-Control") .description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + headerWithName("Connection").description("six"))).document(operationBuilder( + "request-with-headers").request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0").header("Connection", "keep-alive") + .build()); } @Test @@ -83,8 +74,7 @@ public void caseInsensitiveRequestHeaders() throws IOException { .expectRequestHeaders("case-insensitive-request-headers") .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-request-headers", this.snippet.getOutputDirectory()) + "one"))).document(operationBuilder("case-insensitive-request-headers") .request("/").header("X-test", "test").build()); } @@ -97,37 +87,28 @@ public void undocumentedRequestHeader() throws IOException { } @Test - public void missingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()).request("http://localhost").build()); - } - - @Test - public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").build()); + public void requestHeadersWithCustomAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(containsString("Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-title")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(operationBuilder("request-headers-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").header("X-Test", "test").build()); } @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); + this.snippet.expectRequestHeaders( + "request-headers-with-custom-descriptor-attributes").withContents(// + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")).willReturn( snippetResource("request-headers-with-extra-column")); @@ -137,34 +118,12 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { headerWithName("Accept-Encoding").description("two").attributes( key("foo").value("bravo")), headerWithName("Accept").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "request-headers-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") .header("Accept", "*/*").build()); } - @Test - public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-title")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test").build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java new file mode 100644 index 000000000..382e91251 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2016 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; + +/** + * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or + * undocumented headers. + * + * @author Andy Wilkinson + */ +public class ResponseHeadersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "missing-response-headers", this.snippet.getOutputDirectory()).response() + .build()); + } + + @Test + public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", this.snippet + .getOutputDirectory()).response().header("X-Test", "test") + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 24487094f..f58805e01 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,26 +19,20 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link ResponseHeadersSnippet}. @@ -46,13 +40,11 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class ResponseHeadersSnippetTests { +public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public ResponseHeadersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void responseWithHeaders() throws IOException { @@ -65,8 +57,7 @@ public void responseWithHeaders() throws IOException { headerWithName("Content-Type").description("two"), headerWithName("Etag") .description("three"), headerWithName("Cache-Control") .description("five"), headerWithName("Vary").description("six"))) - .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") + .document(operationBuilder("response-headers").response().header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0") @@ -79,9 +70,31 @@ public void caseInsensitiveResponseHeaders() throws IOException { .expectResponseHeaders("case-insensitive-response-headers") .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-response-headers", this.snippet.getOutputDirectory()) - .response().header("X-test", "test").build()); + "one"))).document(operationBuilder("case-insensitive-response-headers").response() + .header("X-test", "test").build()); + } + + @Test + public void undocumentedResponseHeader() throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); + } + + @Test + public void responseHeadersWithCustomAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(containsString("Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(operationBuilder("response-headers-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").build()); } @Test @@ -101,67 +114,12 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { headerWithName("Content-Type").description("two").attributes( key("foo").value("bravo")), headerWithName("Etag").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "response-headers-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .header("X-Test", "test").header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23").build()); } - @Test - public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-title")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").build()); - } - - @Test - public void undocumentedResponseHeader() throws IOException { - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "*/*").build()); - } - - @Test - public void missingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "missing-response-headers", this.snippet.getOutputDirectory()).response() - .build()); - } - - @Test - public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index f948fb103..41e90bfb5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,16 +18,14 @@ import java.io.IOException; -import org.junit.Rule; import org.junit.Test; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; import org.springframework.web.bind.annotation.RequestMethod; import static org.hamcrest.CoreMatchers.containsString; @@ -35,21 +33,20 @@ import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; /** * Tests for {@link HttpRequestSnippet}. * * @author Andy Wilkinson * @author Jonathan Pearlin - * */ -public class HttpRequestSnippetTests { +public class HttpRequestSnippetTests extends AbstractSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public HttpRequestSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void getRequest() throws IOException { @@ -57,9 +54,8 @@ public void getRequest() throws IOException { httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header("Alpha", "a").build()); + new HttpRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost/foo").header("Alpha", "a").build()); } @Test @@ -68,8 +64,7 @@ public void getRequestWithQueryString() throws IOException { httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder( - "get-request-with-query-string", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("get-request-with-query-string") .request("http://localhost/foo?bar=baz").build()); } @@ -81,8 +76,7 @@ public void postRequestWithContent() throws IOException { .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-content", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-content") .request("http://localhost/foo").method("POST").content(content).build()); } @@ -97,8 +91,7 @@ public void postRequestWithCharset() throws IOException { .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-charset", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-charset") .request("http://localhost/foo").method("POST") .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) .build()); @@ -112,8 +105,7 @@ public void postRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-parameter", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") .request("http://localhost/foo").method("POST").param("b&r", "baz") .param("a", "alpha").build()); } @@ -126,8 +118,7 @@ public void putRequestWithContent() throws IOException { .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-content", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("put-request-with-content") .request("http://localhost/foo").method("PUT").content(content).build()); } @@ -139,8 +130,7 @@ public void putRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-parameter", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("put-request-with-parameter") .request("http://localhost/foo").method("PUT").param("b&r", "baz") .param("a", "alpha").build()); } @@ -154,9 +144,8 @@ public void multipartPost() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") + new HttpRequestSnippet().document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()).build()); } @@ -177,8 +166,7 @@ public void multipartPostWithParameters() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("multipart-post-with-parameters") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a", "apple", "avocado").param("b", "banana") @@ -195,8 +183,7 @@ public void multipartPostWithContentType() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("multipart-post-with-content-type") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -208,8 +195,8 @@ public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", - this.snippet.getOutputDirectory()).request("http://localhost/foo") + new HttpRequestSnippet().document(operationBuilder("get-request-custom-host") + .request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com").build()); } @@ -218,13 +205,10 @@ public void requestWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); + given(resolver.resolveTemplateResource("http-request")).willReturn( + snippetResource("http-request-with-title")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) - .document(new OperationBuilder("request-with-snippet-attributes", - this.snippet.getOutputDirectory()) + .document(operationBuilder("request-with-snippet-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost/foo").build()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 4959e7774..96bc7be41 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -18,24 +18,21 @@ import java.io.IOException; -import org.junit.Rule; import org.junit.Test; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; /** * Tests for {@link HttpResponseSnippet}. @@ -43,25 +40,24 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpResponseSnippetTests { +public class HttpResponseSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public HttpResponseSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents( httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(new OperationBuilder("basic-response", - this.snippet.getOutputDirectory()).build()); + new HttpResponseSnippet().document(operationBuilder("basic-response").build()); } @Test public void nonOkResponse() throws IOException { this.snippet.expectHttpResponse("non-ok-response").withContents( httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("non-ok-response").response() .status(HttpStatus.BAD_REQUEST.value()).build()); } @@ -70,8 +66,7 @@ public void responseWithHeaders() throws IOException { this.snippet.expectHttpResponse("response-with-headers").withContents( httpResponse(HttpStatus.OK).header("Content-Type", "application/json") .header("a", "alpha")); - new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("response-with-headers").response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -82,8 +77,8 @@ public void responseWithContent() throws IOException { this.snippet.expectHttpResponse("response-with-content").withContents( httpResponse(HttpStatus.OK).content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpResponseSnippet().document(new OperationBuilder("response-with-content", - this.snippet.getOutputDirectory()).response().content(content).build()); + new HttpResponseSnippet().document(operationBuilder("response-with-content").response() + .content(content).build()); } @Test @@ -95,8 +90,7 @@ public void responseWithCharset() throws IOException { .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); - new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("response-with-charset").response() .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) .build()); } @@ -106,13 +100,10 @@ public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-response")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + given(resolver.resolveTemplateResource("http-response")).willReturn( + snippetResource("http-response-with-title")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) - .document(new OperationBuilder("response-with-snippet-attributes", - this.snippet.getOutputDirectory()).attribute( + .document(operationBuilder("response-with-snippet-attributes").attribute( TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java new file mode 100644 index 000000000..335127cfb --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2016 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.hypermedia; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented + * links. + * + * @author Andy Wilkinson + */ +public class LinksSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedLink() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [foo]")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections.emptyList()).document(new OperationBuilder( + "undocumented-link", this.snippet.getOutputDirectory()).build()); + } + + @Test + public void missingLink() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " found in the response: [foo]")); + new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") + .description("bar"))).document(new OperationBuilder("missing-link", + this.snippet.getOutputDirectory()).build()); + } + + @Test + public void undocumentedLinkAndMissingLink() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [a]. Links with the following relations were not" + + " found in the response: [foo]")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(new OperationBuilder("undocumented-link-and-missing-link", + this.snippet.getOutputDirectory()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 07adce26d..c3bd55a74 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -18,51 +18,29 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link LinksSnippet}. * * @author Andy Wilkinson */ -public class LinksSnippetTests { +public class LinksSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()).document(new OperationBuilder( - "undocumented-link", this.snippet.getOutputDirectory()).build()); + public LinksSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -71,19 +49,8 @@ public void ignoredLink() throws IOException { tableWithHeader("Relation", "Description").row("b", "Link b")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), - new LinkDescriptor("b").description("Link b"))) - .document(new OperationBuilder("ignored-link", this.snippet - .getOutputDirectory()).build()); - } - - @Test - public void missingLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar"))).document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + new LinkDescriptor("b").description("Link b"))).document(operationBuilder( + "ignored-link").build()); } @Test @@ -92,8 +59,7 @@ public void documentedOptionalLink() throws IOException { tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", this.snippet - .getOutputDirectory()).build()); + .document(operationBuilder("documented-optional-link").build()); } @Test @@ -101,20 +67,8 @@ public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar").optional())).document(new OperationBuilder( - "missing-optional-link", this.snippet.getOutputDirectory()).build()); - } - - @Test - public void undocumentedLinkAndMissingLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [a]. Links with the following relations were not" - + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + .description("bar").optional())) + .document(operationBuilder("missing-optional-link").build()); } @Test @@ -125,70 +79,45 @@ public void documentedLinks() throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))) - .document(new OperationBuilder("documented-links", this.snippet - .getOutputDirectory()).build()); + new LinkDescriptor("b").description("two"))).document(operationBuilder( + "documented-links").build()); } @Test - public void linksWithCustomDescriptorAttributes() throws IOException { + public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( - tableWithHeader("Relation", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); + given(resolver.resolveTemplateResource("links")).willReturn( + snippetResource("links-with-title")); + this.snippet.expectLinks("links-with-custom-attributes").withContents( + containsString("Title for the links")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( - new LinkDescriptor("a").description("one").attributes( - key("foo").value("alpha")), - new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "links-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()).attribute(TemplateEngine.class.getName(), + new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")), attributes(key("title") + .value("Title for the links"))).document(operationBuilder( + "links-with-custom-attributes").attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } @Test - public void linksWithCustomAttributes() throws IOException { + public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-title.snippet")); - this.snippet.expectLinks("links-with-custom-attributes").withContents( - startsWith(".Title for the links")); + given(resolver.resolveTemplateResource("links")).willReturn( + snippetResource("links-with-extra-column")); + this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( + tableWithHeader("Relation", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList( - new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")), attributes(key("title") - .value("Title for the links"))).document(new OperationBuilder( - "links-with-custom-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); - } - - private static class StubLinkExtractor implements LinkExtractor { - - private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); - - @Override - public MultiValueMap extractLinks(OperationResponse response) - throws IOException { - return this.linksByRel; - } - - private StubLinkExtractor withLinks(Link... links) { - for (Link link : links) { - this.linksByRel.add(link.getRel(), link); - } - return this; - } - + new LinkDescriptor("a").description("one").attributes( + key("foo").value("alpha")), + new LinkDescriptor("b").description("two").attributes( + key("foo").value("bravo")))).document(operationBuilder( + "links-with-custom-descriptor-attributes").attribute( + TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java new file mode 100644 index 000000000..237cc5b5c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2016 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.hypermedia; + +import java.io.IOException; + +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Stub implementation of {@code LinkExtractor} for testing. + * + * @author Andy Wilkinson + */ +class StubLinkExtractor implements LinkExtractor { + + private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); + + @Override + public MultiValueMap extractLinks(OperationResponse response) + throws IOException { + return this.linksByRel; + } + + StubLinkExtractor withLinks(Link... links) { + for (Link link : links) { + this.linksByRel.add(link.getRel(), link); + } + return this; + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java new file mode 100644 index 000000000..a56e8ed5b --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 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.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link RequestFieldsSnippet} that are specific to Asciidoctor. + * + * @author Andy Wilkinson + */ +public class AsciidoctorRequestFieldsSnippetTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Test + public void requestFieldsWithListDescription() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-fields")).willReturn( + snippetResource("request-fields-with-list-description")); + this.snippet.expectRequestFields("request-fields-with-list-description") + .withContents( + tableWithHeader(asciidoctor(), "Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( + Arrays.asList("one", "two")))) + .document(new OperationBuilder("request-fields-with-list-description", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").content("{\"a\": \"foo\"}").build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/adoc/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java new file mode 100644 index 000000000..442616b61 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2016 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.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link RequestFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Andy Wilkinson + */ +public class RequestFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new RequestFieldsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-request-field", this.snippet + .getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5}").build()); + } + + @Test + public void missingRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a.b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(new OperationBuilder("missing-request-fields", this.snippet + .getOutputDirectory()).request("http://localhost").content("{}") + .build()); + } + + @Test + public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .optional())).document(new OperationBuilder( + "missing-optional-request-field-with-no-type", this.snippet + .getOutputDirectory()).request("http://localhost").content("{ }") + .build()); + } + + @Test + public void undocumentedRequestFieldAndMissingRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a.b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(new OperationBuilder( + "undocumented-request-field-and-missing-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{ \"a\": { \"c\": 5 }}").build()); + } + + @Test + public void undocumentedXmlRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new RequestFieldsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-xml-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void xmlRequestFieldWithNoType() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("missing-xml-request", this.snippet + .getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-request-fields", this.snippet.getOutputDirectory()) + .request("http://localhost").content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a/b]")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 0fedbdee4..7f9a665a6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,43 +18,33 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link RequestFieldsSnippet}. * * @author Andy Wilkinson */ -public class RequestFieldsSnippetTests { +public class RequestFieldsSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public RequestFieldsSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void mapRequestWithFields() throws IOException { @@ -65,9 +55,8 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))).document(new OperationBuilder( - "map-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") + fieldWithPath("a").description("three"))).document(operationBuilder( + "map-request-with-fields").request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -80,24 +69,11 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a") - .description("three"))).document(new OperationBuilder( - "array-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") + .description("three"))).document(operationBuilder( + "array-request-with-fields").request("http://localhost") .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); } - @Test - public void undocumentedRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); - } - @Test public void ignoredRequestField() throws IOException { this.snippet.expectRequestFields("ignored-request-field").withContents( @@ -105,48 +81,25 @@ public void ignoredRequestField() throws IOException { "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); + fieldWithPath("b").description("Field b"))).document(operationBuilder( + "ignored-request-field").request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); } @Test - public void missingRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a.b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields", this.snippet - .getOutputDirectory()).request("http://localhost").content("{}") - .build()); - } - - @Test - public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", this.snippet - .getOutputDirectory()).request("http://localhost").content("{ }") - .build()); - } + public void requestFieldsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-fields")).willReturn( + snippetResource("request-fields-with-title")); + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents(containsString("Custom title")); - @Test - public void undocumentedRequestFieldAndMissingRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a.b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder( - "undocumented-request-field-and-missing-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))).document(operationBuilder( + "request-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test @@ -167,52 +120,13 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-fields-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "request-fields-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } - @Test - public void requestFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); - } - - @Test - public void requestFieldsWithListDescription() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader("Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( - Arrays.asList("one", "two")))) - .document(new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); - } - @Test public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request").withContents( @@ -221,78 +135,11 @@ public void xmlRequestFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlRequestFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-request-fields", this.snippet.getOutputDirectory()) - .request("http://localhost").content("") + fieldWithPath("a").description("three").type("a"))).document(operationBuilder( + "xml-request").request("http://localhost") + .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java new file mode 100644 index 000000000..d664b8074 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2016 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.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Andy Wilkinson + */ +public class ResponseFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedXmlResponseField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new ResponseFieldsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlAttribute() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", this.snippet + .getOutputDirectory()) + .response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void documentedXmlAttributesAreRemoved() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo(String + .format("The following parts of the payload were not documented:" + + "%nbar%n"))); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", this.snippet.getOutputDirectory()) + .response().content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void xmlResponseFieldWithNoType() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("xml-response-no-field-type", this.snippet + .getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlResponseField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/b]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-response-field", this.snippet.getOutputDirectory()) + .response().content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedXmlResponseFieldAndMissingXmlResponseField() + throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + this.thrown + .expectMessage(endsWith("Fields with the following paths were not found" + + " in the payload: [a/b]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ad6653d38..f4e00111d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,43 +18,33 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link ResponseFieldsSnippet}. * * @author Andy Wilkinson */ -public class ResponseFieldsSnippetTests { +public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public ResponseFieldsSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void mapResponseWithFields() throws IOException { @@ -69,14 +59,12 @@ public void mapResponseWithFields() throws IOException { .description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), - fieldWithPath("assets[].name").description("six"))) - .document(new OperationBuilder("map-response-with-fields", this.snippet - .getOutputDirectory()) - .response() - .content( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); + fieldWithPath("assets[].name").description("six"))).document(operationBuilder( + "map-response-with-fields") + .response() + .content( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}").build()); } @Test @@ -88,8 +76,7 @@ public void arrayResponseWithFields() throws IOException { new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") .description("two"), fieldWithPath("[]a").description("three"))) - .document(new OperationBuilder("array-response-with-fields", this.snippet - .getOutputDirectory()).response() + .document(operationBuilder("array-response-with-fields").response() .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); } @@ -101,8 +88,7 @@ public void arrayResponse() throws IOException { tableWithHeader("Path", "Type", "Description").row("[]", "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(new OperationBuilder("array-response", this.snippet - .getOutputDirectory()).response() + .document(operationBuilder("array-response").response() .content("[\"a\", \"b\", \"c\"]").build()); } @@ -113,10 +99,25 @@ public void ignoredResponseField() throws IOException { "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-response-field", this.snippet - .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") - .build()); + fieldWithPath("b").description("Field b"))).document(operationBuilder( + "ignored-response-field").response().content("{\"a\": 5, \"b\": 4}") + .build()); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-fields")).willReturn( + snippetResource("response-fields-with-title")); + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents(containsString("Custom title")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))).document(operationBuilder( + "response-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .content("{\"a\": \"foo\"}").build()); } @Test @@ -137,31 +138,13 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-fields-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "response-fields-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } - @Test - public void responseFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": \"foo\"}").build()); - } - @Test public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response").withContents( @@ -169,28 +152,10 @@ public void xmlResponseFields() throws IOException { .row("a/c", "c", "two").row("a", "a", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-response", this.snippet - .getOutputDirectory()) - .response() - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlResponseField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new ResponseFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("three").type("a"))).document(operationBuilder( + "xml-response").response().content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -200,30 +165,13 @@ public void xmlAttribute() throws IOException { "a/@id", "c", "two")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("xml-attribute", this.snippet - .getOutputDirectory()) + .document(operationBuilder("xml-attribute") .response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void missingXmlAttribute() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/@id]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - @Test public void missingOptionalXmlAttribute() throws IOException { this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( @@ -231,8 +179,7 @@ public void missingOptionalXmlAttribute() throws IOException { "a/@id", "c", "two")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") .type("b"), fieldWithPath("a/@id").description("two").type("c") - .optional())).document(new OperationBuilder( - "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) + .optional())).document(operationBuilder("missing-optional-xml-attribute") .response().content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -243,76 +190,10 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("a"))).document(new OperationBuilder("undocumented-attribute", - this.snippet.getOutputDirectory()).response() + .type("a"))).document(operationBuilder("undocumented-attribute").response() .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void documentedXmlAttributesAreRemoved() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo(String - .format("The following parts of the payload were not documented:" - + "%nbar%n"))); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", this.snippet.getOutputDirectory()) - .response().content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlResponseFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", this.snippet - .getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlResponseField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-response-field", this.snippet.getOutputDirectory()) - .response().content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlResponseFieldAndMissingXmlResponseField() - throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java new file mode 100644 index 000000000..7f9a07b84 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2016 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.request; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +/** + * Tests for failures when rendering {@link PathParametersSnippet} due to missing or + * undocumented path parameters. + * + * @author Andy Wilkinson + */ +public class PathParametersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedPathParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [a]")); + new PathParametersSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-path-parameter", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + } + + @Test + public void missingPathParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not found in the request: [a]")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("missing-path-parameter", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/").build()); + } + + @Test + public void undocumentedAndMissingPathParameters() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Path parameters with the following names were" + + " not documented: [b]. Path parameters with the following" + + " names were not found in the request: [a]")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder( + "undocumented-and-missing-path-parameters", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{b}").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 4f2ee0b23..9d148769e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -18,78 +18,30 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; /** * Tests for {@link PathParametersSnippet}. * * @author Andy Wilkinson - * */ -public class PathParametersSnippetTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); +public class PathParametersSnippetTests extends AbstractSnippetTests { - @Test - public void undocumentedPathParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [a]")); - new PathParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); - } - - @Test - public void missingPathParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not found in the request: [a]")); - new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); - } - - @Test - public void undocumentedAndMissingPathParameters() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [b]. Path parameters with the following" - + " names were not found in the request: [a]")); - new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder( - "undocumented-and-missing-path-parameters", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + public PathParametersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -99,9 +51,9 @@ public void pathParameters() throws IOException { "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + .description("two"))).document(operationBuilder("path-parameters") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .build()); } @Test @@ -110,10 +62,9 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + parameterWithName("b").description("two"))).document(operationBuilder( + "ignored-path-parameter").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); } @Test @@ -124,57 +75,47 @@ public void pathParametersWithQueryString() throws IOException { .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))) - .document(new OperationBuilder("path-parameters-with-query-string", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar") - .build()); + .description("two"))).document(operationBuilder( + "path-parameters-with-query-string").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); } @Test - public void pathParametersWithCustomDescriptorAttributes() throws IOException { + public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters( - "path-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + snippetResource("path-parameters-with-title")); + this.snippet.expectPathParameters("path-parameters-with-custom-attributes") + .withContents(containsString("The title")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) - .document(new OperationBuilder( - "path-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document(operationBuilder( + "path-parameters-with-custom-attributes") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); + } @Test - public void pathParametersWithCustomAttributes() throws IOException { + public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); + snippetResource("path-parameters-with-extra-column")); + this.snippet.expectPathParameters( + "path-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(new OperationBuilder("path-parameters-with-custom-attributes", - this.snippet.getOutputDirectory()) + .description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder("path-parameters-with-custom-descriptor-attributes") .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); - - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java new file mode 100644 index 000000000..b795dd572 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2016 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.request; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +/** + * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or + * undocumented request parameters. + * + * @author Andy Wilkinson + */ +public class RequestParametersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not documented: [a]")); + new RequestParametersSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-parameter", this.snippet + .getOutputDirectory()).request("http://localhost") + .param("a", "alpha").build()); + } + + @Test + public void missingParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not found in the request: [a]")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document(new OperationBuilder("missing-parameter", this.snippet + .getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedAndMissingParameters() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not documented: [b]. Request parameters with the following" + + " names were not found in the request: [a]")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document(new OperationBuilder( + "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost").param("b", "bravo").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index da61deecc..2facc40c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -18,75 +18,30 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link RequestParametersSnippet}. * * @author Andy Wilkinson */ -public class RequestParametersSnippetTests { +public class RequestParametersSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Test - public void undocumentedParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [a]")); - new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); - } - - @Test - public void missingParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder("missing-parameter", this.snippet - .getOutputDirectory()).request("http://localhost").build()); - } - - @Test - public void undocumentedAndMissingParameters() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [b]. Request parameters with the following" - + " names were not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder( - "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo").build()); + public RequestParametersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -96,8 +51,7 @@ public void requestParameters() throws IOException { "two")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "request-parameters", this.snippet.getOutputDirectory()) + .description("two"))).document(operationBuilder("request-parameters") .request("http://localhost").param("a", "bravo").param("b", "bravo") .build()); } @@ -107,58 +61,50 @@ public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( tableWithHeader("Parameter", "Description").row("b", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-request-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); + parameterWithName("b").description("two"))).document(operationBuilder( + "ignored-request-parameter").request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test - public void requestParametersWithCustomDescriptorAttributes() throws IOException { + public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + snippetResource("request-parameters-with-title")); + this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") + .withContents(containsString("The title")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "request-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("bravo"))), attributes(key("title").value( + "The title"))).document(operationBuilder( + "request-parameters-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .param("a", "alpha").param("b", "bravo").build()); } @Test - public void requestParametersWithCustomAttributes() throws IOException { + public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); + snippetResource("request-parameters-with-extra-column")); + this.snippet.expectRequestParameters( + "request-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))), attributes(key("title").value( - "The title"))).document(new OperationBuilder( - "request-parameters-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("bravo")))).document(operationBuilder( + "request-parameters-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .param("a", "alpha").param("b", "bravo").build()); } - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 55ae2218e..31bdb75dc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -26,6 +26,7 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; /** * Tests for {@link StandardWriterResolver}. @@ -37,7 +38,7 @@ public class StandardWriterResolverTests { private final PlaceholderResolver placeholderResolver = mock(PlaceholderResolver.class); private final StandardWriterResolver resolver = new StandardWriterResolver( - this.placeholderResolver); + this.placeholderResolver, "UTF-8", asciidoctor()); @Test public void noConfiguredOutputDirectoryAndRelativeInput() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index ef807ee20..00a3c8d4a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -29,6 +29,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; /** * Tests for {@link TemplateResourceResolver}. @@ -40,7 +41,8 @@ public class StandardTemplateResourceResolverTests { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver(); + private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver( + asciidoctor()); private final TestClassLoader classLoader = new TestClassLoader(); @@ -66,7 +68,7 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/default-test.snippet", getClass() + "org/springframework/restdocs/templates/adoc/test.snippet", getClass() .getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 63364f6a6..6a571f350 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,6 +23,7 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; @@ -38,14 +39,21 @@ */ public class ExpectedSnippet implements TestRule { + private final SnippetFormat snippetFormat; + + private final SnippetMatcher snippet; + private String expectedName; private String expectedType; - private SnippetMatcher snippet = SnippetMatchers.snippet(); - private File outputDirectory; + public ExpectedSnippet(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + this.snippet = SnippetMatchers.snippet(snippetFormat); + } + @Override public Statement apply(final Statement base, Description description) { this.outputDirectory = new File("build/" @@ -56,7 +64,8 @@ public Statement apply(final Statement base, Description description) { private void verifySnippet() throws IOException { if (this.outputDirectory != null && this.expectedName != null) { File snippetDir = new File(this.outputDirectory, this.expectedName); - File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); + File snippetFile = new File(snippetDir, this.expectedType + "." + + this.snippetFormat.getFileExtension()); assertThat(snippetFile, is(this.snippet)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e5be81665..e2a54b998 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -37,6 +37,8 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -58,11 +60,18 @@ public class OperationBuilder { private final File outputDirectory; + private final SnippetFormat snippetFormat; + private OperationRequestBuilder requestBuilder; public OperationBuilder(String name, File outputDirectory) { + this(name, outputDirectory, SnippetFormats.asciidoctor()); + } + + public OperationBuilder(String name, File outputDirectory, SnippetFormat snippetFormat) { this.name = name; this.outputDirectory = outputDirectory; + this.snippetFormat = snippetFormat; } public OperationRequestBuilder request(String uri) { @@ -82,13 +91,15 @@ public OperationBuilder attribute(String name, Object value) { public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { this.attributes.put(TemplateEngine.class.getName(), - new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + new MustacheTemplateEngine(new StandardTemplateResourceResolver( + this.snippetFormat))); } RestDocumentationContext context = new RestDocumentationContext(null, null, this.outputDirectory); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context))); + new RestDocumentationContextPlaceholderResolver(context), "UTF-8", + this.snippetFormat)); return new StandardOperation(this.name, (this.requestBuilder == null ? new OperationRequestBuilder( "http://localhost/").buildRequest() : this.requestBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 97818a52e..c076d0ca7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -30,6 +30,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -45,37 +47,62 @@ private SnippetMatchers() { } - public static SnippetMatcher snippet() { - return new SnippetMatcher(); + public static SnippetMatcher snippet(SnippetFormat snippetFormat) { + return new SnippetMatcher(snippetFormat); } - public static AsciidoctorTableMatcher tableWithTitleAndHeader(String title, - String... headers) { - return new AsciidoctorTableMatcher(title, headers); + public static TableMatcher tableWithHeader(SnippetFormat format, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableMatcher(null, headers); + } + return new MarkdownTableMatcher(null, headers); } - public static AsciidoctorTableMatcher tableWithHeader(String... headers) { - return new AsciidoctorTableMatcher(null, headers); + public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, + String title, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableMatcher(title, headers); + } + return new MarkdownTableMatcher(title, headers); } - public static HttpRequestMatcher httpRequest(RequestMethod method, String uri) { - return new HttpRequestMatcher(method, uri); + public static HttpRequestMatcher httpRequest(SnippetFormat format, + RequestMethod requestMethod, String uri) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpRequestMatcher(requestMethod, uri, + new AsciidoctorCodeBlockMatcher<>("http"), 3); + } + return new HttpRequestMatcher(requestMethod, uri, new MarkdownCodeBlockMatcher<>( + "http"), 2); } - public static HttpResponseMatcher httpResponse(HttpStatus status) { - return new HttpResponseMatcher(status); + public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus status) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpResponseMatcher(status, new AsciidoctorCodeBlockMatcher<>( + "http"), 3); + } + return new HttpResponseMatcher(status, new MarkdownCodeBlockMatcher<>("http"), 2); } @SuppressWarnings({ "rawtypes" }) - public static AsciidoctorCodeBlockMatcher codeBlock(String language) { - return new AsciidoctorCodeBlockMatcher(language); + public static CodeBlockMatcher codeBlock(SnippetFormat format, String language) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockMatcher(language); + } + return new MarkdownCodeBlockMatcher(language); } private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { + private final SnippetFormat snippetFormat; + private List lines = new ArrayList<>(); + protected AbstractSnippetContentMatcher(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + protected void addLine(String line) { this.lines.add(line); } @@ -94,7 +121,7 @@ public boolean matches(Object item) { @Override public void describeTo(Description description) { - description.appendText("Asciidoctor snippet"); + description.appendText(this.snippetFormat.getFileExtension() + " snippet"); description.appendText(getLinesAsString()); } @@ -122,24 +149,55 @@ private String getLinesAsString() { } } + /** + * Base class for code block matchers. + * + * @param The type of the matcher + */ + public static class CodeBlockMatcher> extends + AbstractSnippetContentMatcher { + + protected CodeBlockMatcher(SnippetFormat snippetFormat) { + super(snippetFormat); + } + + @SuppressWarnings("unchecked") + public T content(String content) { + this.addLine(-1, content); + return (T) this; + } + + } + /** * A {@link Matcher} for an Asciidoctor code block. * * @param The type of the matcher */ public static class AsciidoctorCodeBlockMatcher> - extends AbstractSnippetContentMatcher { + extends CodeBlockMatcher { protected AsciidoctorCodeBlockMatcher(String language) { + super(SnippetFormats.asciidoctor()); this.addLine("[source," + language + "]"); this.addLine("----"); this.addLine("----"); } - @SuppressWarnings("unchecked") - public T content(String content) { - this.addLine(-1, content); - return (T) this; + } + + /** + * A {@link Matcher} for a Markdown code block. + * + * @param The type of the matcher + */ + public static class MarkdownCodeBlockMatcher> + extends CodeBlockMatcher { + + protected MarkdownCodeBlockMatcher(String language) { + super(SnippetFormats.markdown()); + this.addLine("```" + language); + this.addLine("```"); } } @@ -150,26 +208,45 @@ public T content(String content) { * @param The type of the matcher */ public static abstract class HttpMatcher> extends - AsciidoctorCodeBlockMatcher> { + BaseMatcher { + + private final CodeBlockMatcher delegate; - private int headerOffset = 3; + private int headerOffset; - protected HttpMatcher() { - super("http"); + protected HttpMatcher(CodeBlockMatcher delegate, int headerOffset) { + this.delegate = delegate; + this.headerOffset = headerOffset; } @SuppressWarnings("unchecked") public T header(String name, String value) { - this.addLine(this.headerOffset++, name + ": " + value); + this.delegate.addLine(this.headerOffset++, name + ": " + value); return (T) this; } @SuppressWarnings("unchecked") public T header(String name, long value) { - this.addLine(this.headerOffset++, name + ": " + value); + this.delegate.addLine(this.headerOffset++, name + ": " + value); return (T) this; } + @SuppressWarnings("unchecked") + public T content(String content) { + this.delegate.addLine(-1, content); + return (T) this; + } + + @Override + public boolean matches(Object item) { + return this.delegate.matches(item); + } + + @Override + public void describeTo(Description description) { + this.delegate.describeTo(description); + } + } /** @@ -178,7 +255,9 @@ public T header(String name, long value) { public static final class HttpResponseMatcher extends HttpMatcher { - private HttpResponseMatcher(HttpStatus status) { + private HttpResponseMatcher(HttpStatus status, CodeBlockMatcher delegate, + int headerOffset) { + super(delegate, headerOffset); this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); this.content(""); } @@ -190,20 +269,41 @@ private HttpResponseMatcher(HttpStatus status) { */ public static final class HttpRequestMatcher extends HttpMatcher { - private HttpRequestMatcher(RequestMethod requestMethod, String uri) { + private HttpRequestMatcher(RequestMethod requestMethod, String uri, + CodeBlockMatcher delegate, int headerOffset) { + super(delegate, headerOffset); this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); this.content(""); } } + /** + * Base class for table matchers. + * + * @param The concrete type of the matcher + */ + public static abstract class TableMatcher> extends + AbstractSnippetContentMatcher { + + protected TableMatcher(SnippetFormat snippetFormat) { + super(snippetFormat); + } + + public abstract T row(String... entries); + + public abstract T configuration(String configuration); + + } + /** * A {@link Matcher} for an Asciidoctor table. */ public static final class AsciidoctorTableMatcher extends - AbstractSnippetContentMatcher { + TableMatcher { private AsciidoctorTableMatcher(String title, String... columns) { + super(SnippetFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine("." + title); } @@ -216,6 +316,7 @@ private AsciidoctorTableMatcher(String title, String... columns) { this.addLine("|==="); } + @Override public AsciidoctorTableMatcher row(String... entries) { for (String entry : entries) { this.addLine(-1, "|" + entry); @@ -224,19 +325,68 @@ public AsciidoctorTableMatcher row(String... entries) { return this; } + @Override public AsciidoctorTableMatcher configuration(String configuration) { this.addLine(0, configuration); return this; } + + } + + /** + * A {@link Matcher} for a Markdown table. + */ + public static final class MarkdownTableMatcher extends + TableMatcher { + + private MarkdownTableMatcher(String title, String... columns) { + super(SnippetFormats.asciidoctor()); + if (StringUtils.hasText(title)) { + this.addLine(title); + } + String header = StringUtils.collectionToDelimitedString( + Arrays.asList(columns), " | "); + this.addLine(header); + List components = new ArrayList<>(); + for (String column : columns) { + StringBuilder dashes = new StringBuilder(); + for (int i = 0; i < column.length(); i++) { + dashes.append("-"); + } + components.add(dashes.toString()); + } + this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); + this.addLine(""); + } + + @Override + public MarkdownTableMatcher row(String... entries) { + this.addLine(-1, StringUtils.collectionToDelimitedString( + Arrays.asList(entries), " | ")); + return this; + } + + @Override + public MarkdownTableMatcher configuration(String configuration) { + throw new UnsupportedOperationException( + "Markdown does not support table configuration"); + } + } /** * A {@link Matcher} for a snippet file. */ - public static class SnippetMatcher extends BaseMatcher { + public static final class SnippetMatcher extends BaseMatcher { + + private final SnippetFormat snippetFormat; private Matcher expectedContents; + private SnippetMatcher(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + @Override public boolean matches(Object item) { if (snippetFileExists(item)) { @@ -285,7 +435,8 @@ public void describeTo(Description description) { this.expectedContents.describeTo(description); } else { - description.appendText("Asciidoctor snippet"); + description + .appendText(this.snippetFormat.getFileExtension() + " snippet"); } } @@ -295,4 +446,5 @@ public SnippetMatcher withContents(Matcher matcher) { } } + } diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet new file mode 100644 index 000000000..483ab6c7f --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet @@ -0,0 +1,4 @@ +{{title}} +```bash +$ curl {{url}} {{options}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet new file mode 100644 index 000000000..363a93694 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet @@ -0,0 +1,8 @@ +{{title}} +```http +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet new file mode 100644 index 000000000..103fa6dcb --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet @@ -0,0 +1,8 @@ +{{title}} +```http +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet new file mode 100644 index 000000000..a7e3349d3 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet @@ -0,0 +1,5 @@ +Relation | Description | Foo +-------- | ----------- | --- +{{#links}} +{{rel}} | {{description}} | {{foo}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet new file mode 100644 index 000000000..ecaaeb33b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Relation | Description +-------- | ----------- +{{#links}} +{{rel}} | {{description}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet new file mode 100644 index 000000000..260502b69 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet new file mode 100644 index 000000000..b63b62353 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet new file mode 100644 index 000000000..e4ec4c605 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet @@ -0,0 +1,5 @@ +Path | Type | Description | Foo +---- | ---- | ----------- | --- +{{#fields}} +{{path}} | {{type}} | {{description}} | {{foo}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet new file mode 100644 index 000000000..24bb63fa9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet new file mode 100644 index 000000000..af392f2a8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#headers}} +{{name}} | {{description}} | {{foo}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet new file mode 100644 index 000000000..d57ed7ba8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet new file mode 100644 index 000000000..260502b69 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet new file mode 100644 index 000000000..b63b62353 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet new file mode 100644 index 000000000..e4ec4c605 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet @@ -0,0 +1,5 @@ +Path | Type | Description | Foo +---- | ---- | ----------- | --- +{{#fields}} +{{path}} | {{type}} | {{description}} | {{foo}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet new file mode 100644 index 000000000..24bb63fa9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet new file mode 100644 index 000000000..af392f2a8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#headers}} +{{name}} | {{description}} | {{foo}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet new file mode 100644 index 000000000..d57ed7ba8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index f9beacace..bb7d7071c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -67,8 +67,8 @@ public UriConfigurer uris() { public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, - Arrays.asList(getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), snippets(), this.uriConfigurer)); + Arrays.asList(snippets(), getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), this.uriConfigurer)); } @Override diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index d444dd177..0c48c4801 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -75,6 +75,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.snippet.SnippetFormats.markdown; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -123,6 +125,20 @@ public void basicSnippetGeneration() throws Exception { "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } + @Test + public void markdownSnippetGeneration() throws Exception { + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .snippets().withEncoding("UTF-8").withFormat(markdown())).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("basic-markdown")); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/basic-markdown"), "http-request.md", + "http-response.md", "curl-request.md"); + } + @Test public void curlSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -130,10 +146,11 @@ public void curlSnippetWithContent() throws Exception { mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); - assertThat(new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:8080/' -i -X POST " + "-H 'Accept: application/json' -d 'content'")))); } @@ -150,8 +167,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:8080/?foo=bar' -i -X POST " + "-H 'Accept: application/json' -d 'a=alpha'")))); @@ -298,9 +315,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("a", "alpha") - .header("b", "bravo") + is(snippet(asciidoctor()).withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("a", "alpha").header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Host", "localhost") @@ -310,8 +327,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("b", "bravo") + is(snippet(asciidoctor()).withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .content(prettyPrinted)))); @@ -337,8 +355,8 @@ public void preprocessedResponse() throws Exception { + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("a", "alpha") .header("Content-Type", "application/json") .header(HttpHeaders.CONTENT_LENGTH, @@ -348,8 +366,8 @@ public void preprocessedResponse() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("Content-Type", "application/json") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) @@ -376,7 +394,7 @@ public void customSnippetTemplate() throws Exception { } assertThat(new File( "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet().withContents(equalTo("Custom curl request")))); + is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); mockMvc.perform(get("/")).andDo( document( @@ -395,9 +413,9 @@ public void customContextPath() throws Exception { .andExpect(status().isOk()).andDo(document("custom-context-path")); assertThat( new File("build/generated-snippets/custom-context-path/curl-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - codeBlock("bash") + codeBlock(asciidoctor(), "bash") .content( "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 5dbdb1c09..256d65271 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -51,8 +51,8 @@ public final class RestAssuredRestDocumentationConfigurer RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = Arrays.asList(getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), snippets()); + this.configurers = Arrays.asList(snippets(), getTemplateEngineConfigurer(), + getWriterResolverConfigurer()); } @Override diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 3ae6d13f5..eeea60b48 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -70,6 +70,7 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -114,8 +115,8 @@ public void curlSnippetWithContent() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl 'http://localhost:" + this.port + "/' -i " + "-X POST -H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " @@ -133,8 +134,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:" + this.port + "/?foo=bar' -i -X POST " + "-H 'Accept: application/json' " @@ -261,9 +262,9 @@ public void preprocessedRequest() throws Exception { .then().statusCode(200); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - httpRequest(RequestMethod.GET, "/") + httpRequest(asciidoctor(), RequestMethod.GET, "/") .header("a", "alpha") .header("b", "bravo") .header("Accept", @@ -277,9 +278,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - httpRequest(RequestMethod.GET, "/") + httpRequest(asciidoctor(), RequestMethod.GET, "/") .header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) @@ -308,8 +309,8 @@ public void preprocessedResponse() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("Foo", "https://api.example.com/foo/bar") .header("Content-Type", "application/json;charset=UTF-8") .header(HttpHeaders.CONTENT_LENGTH, @@ -335,7 +336,7 @@ public void customSnippetTemplate() throws Exception { } assertThat(new File( "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet().withContents(equalTo("Custom curl request")))); + is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { From 28476c2d08aae9a139f1ae152fe374e2cb1b6bd7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Feb 2016 14:09:20 +0000 Subject: [PATCH 0211/1059] Polish RestDocumentationConfigurer and improve its test coverage --- .../config/RestDocumentationConfigurer.java | 28 +-- .../RestDocumentationConfigurerTests.java | 180 ++++++++++++++++++ .../MockMvcRestDocumentationConfigurer.java | 21 +- ...estAssuredRestDocumentationConfigurer.java | 11 +- 4 files changed, 201 insertions(+), 39 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 0f0ff9b21..ee721725b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.config; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; @@ -34,7 +36,7 @@ * support chaining * @author Andy Wilkinson */ -public abstract class RestDocumentationConfigurer { +public abstract class RestDocumentationConfigurer { private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); @@ -74,21 +76,19 @@ public final T writerResolver(WriterResolver writerResolver) { } /** - * Returns the configurer used to configure the context with a {@link WriterResolver}. + * Applies this configurer to the given {@code configuration} within the given + * {@code context}. * - * @return the configurer + * @param configuration the configuration + * @param context the current context */ - protected final AbstractConfigurer getWriterResolverConfigurer() { - return this.writerResolverConfigurer; - } - - /** - * Returns the configurer used to configure the context with a {@link TemplateEngine}. - * - * @return the configurer - */ - protected final AbstractConfigurer getTemplateEngineConfigurer() { - return this.templateEngineConfigurer; + protected final void apply(Map configuration, + RestDocumentationContext context) { + List configurers = Arrays.asList(snippets(), + this.templateEngineConfigurer, this.writerResolverConfigurer); + for (AbstractConfigurer configurer : configurers) { + configurer.apply(configuration, context); + } } private static final class TemplateEngineConfigurer extends AbstractConfigurer { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java new file mode 100644 index 000000000..2dad00fe9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2012-2016 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.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.http.HttpRequestSnippet; +import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationConfigurerTests { + + private final TestRestDocumentationConfigurer configurer = new TestRestDocumentationConfigurer(); + + @SuppressWarnings("unchecked") + @Test + public void defaultConfiguration() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(TemplateEngine.class.getName()), + instanceOf(MustacheTemplateEngine.class))); + assertThat( + configuration, + hasEntry(equalTo(WriterResolver.class.getName()), + instanceOf(StandardWriterResolver.class))); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + List defaultSnippets = (List) configuration + .get("org.springframework.restdocs.defaultSnippets"); + assertThat( + defaultSnippets, + contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpRequestSnippet.class), + instanceOf(HttpResponseSnippet.class))); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding(), is(equalTo("UTF-8"))); + assertThat(snippetConfiguration.getFormat(), + is(equalTo(SnippetFormats.asciidoctor()))); + } + + @Test + public void customTemplateEngine() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + TemplateEngine templateEngine = mock(TemplateEngine.class); + this.configurer.templateEngine(templateEngine).apply(configuration, context); + assertThat(configuration, Matchers.hasEntry( + TemplateEngine.class.getName(), templateEngine)); + } + + @Test + public void customWriterResolver() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + WriterResolver writerResolver = mock(WriterResolver.class); + this.configurer.writerResolver(writerResolver).apply(configuration, context); + assertThat(configuration, Matchers.hasEntry( + WriterResolver.class.getName(), writerResolver)); + } + + @Test + public void customDefaultSnippets() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withDefaults(CurlDocumentation.curlRequest()) + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + @SuppressWarnings("unchecked") + List defaultSnippets = (List) configuration + .get("org.springframework.restdocs.defaultSnippets"); + assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); + } + + @Test + public void customSnippetEncoding() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withEncoding("ISO 8859-1") + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding(), is(equalTo("ISO 8859-1"))); + } + + @Test + public void customSnippetFormat() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withFormat(SnippetFormats.markdown()) + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getFormat(), + is(equalTo(SnippetFormats.markdown()))); + } + + private static final class TestRestDocumentationConfigurer + extends + RestDocumentationConfigurer { + + private final TestSnippetConfigurer snippetConfigurer = new TestSnippetConfigurer( + this); + + @Override + public TestSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + } + + private static final class TestSnippetConfigurer extends + SnippetConfigurer { + + private TestSnippetConfigurer(TestRestDocumentationConfigurer parent) { + super(parent); + } + + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index bb7d7071c..a402487fe 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -16,15 +16,12 @@ package org.springframework.restdocs.mockmvc; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.AbstractConfigurer; import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; @@ -66,9 +63,7 @@ public UriConfigurer uris() { @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, - Arrays.asList(snippets(), getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), this.uriConfigurer)); + return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation); } @Override @@ -81,17 +76,13 @@ public MockMvcSnippetConfigurer snippets() { return this.snippetConfigurer; } - private static final class ConfigurerApplyingRequestPostProcessor implements + private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { private final RestDocumentation restDocumentation; - private final List configurers; - - private ConfigurerApplyingRequestPostProcessor( - RestDocumentation restDocumentation, List configurers) { + private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = configurers; } @Override @@ -105,9 +96,9 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) request.getAttribute(urlTemplateAttribute)); request.setAttribute("org.springframework.restdocs.configuration", configuration); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(configuration, context); - } + MockMvcRestDocumentationConfigurer.this.apply(configuration, context); + MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, + context); return request; } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 256d65271..25afe0ec2 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -16,14 +16,11 @@ package org.springframework.restdocs.restassured; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.AbstractConfigurer; import org.springframework.restdocs.config.RestDocumentationConfigurer; import com.jayway.restassured.filter.Filter; @@ -45,14 +42,10 @@ public final class RestAssuredRestDocumentationConfigurer private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( this); - private final List configurers; - private final RestDocumentation restDocumentation; RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = Arrays.asList(snippets(), getTemplateEngineConfigurer(), - getWriterResolverConfigurer()); } @Override @@ -68,9 +61,7 @@ public Response filter(FilterableRequestSpecification requestSpec, Map configuration = new HashMap<>(); filterContext.setValue("org.springframework.restdocs.configuration", configuration); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(configuration, context); - } + apply(configuration, context); return filterContext.next(requestSpec, responseSpec); } } From 27f919748bdf70b4bebb645a873cd3c5777fab89 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 3 Feb 2016 17:18:44 +0000 Subject: [PATCH 0212/1059] Upgrade to the latest version of the Spring IO Plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a286bdeed..75fd5ae99 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ buildscript { maven { url 'https://repo.spring.io/plugins-release' } } dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' + classpath 'io.spring.gradle:dependency-management-plugin:0.5.5.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' + classpath 'io.spring.gradle:spring-io-plugin:0.0.5.RELEASE' } } From a9c9bad15ef03e4a0b72d6c11c6725a9cc140be5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Feb 2016 11:33:40 +0000 Subject: [PATCH 0213/1059] Reduce duplication by introducing RestDocumentationHandler Previously, logic for creating an Operation, determining the snippets to call, and calling them was duplicated in both the MockMvc and REST Assured modules. This commit introduces a new core class, RestDocumentationHandler, that now does the bulk of the work in a reusable manner. The MockMvc and REST Assured modules have been updated to delegate to RestDocumentationHandler. Closes gh-194 --- .../restdocs/RestDocumentationException.java | 47 ++++ .../restdocs/RestDocumentationHandler.java | 232 ++++++++++++++++++ .../operation/ConversionException.java | 50 ++++ .../restdocs/operation/RequestConverter.java | 37 +++ .../restdocs/operation/ResponseConverter.java | 37 +++ .../RestDocumentationHandlerTests.java | 130 ++++++++++ ...tory.java => MockMvcRequestConverter.java} | 51 ++-- ...ory.java => MockMvcResponseConverter.java} | 17 +- .../mockmvc/MockMvcRestDocumentation.java | 23 +- .../MockMvcRestDocumentationConfigurer.java | 2 +- .../RestDocumentationResultHandler.java | 109 +------- ...java => MockMvcRequestConverterTests.java} | 21 +- ....java => RestAssuredRequestConverter.java} | 9 +- ...java => RestAssuredResponseConverter.java} | 8 +- .../RestAssuredRestDocumentation.java | 19 +- .../restassured/RestDocumentationFilter.java | 116 ++------- ... => RestAssuredRequestConverterTests.java} | 34 +-- 17 files changed, 662 insertions(+), 280 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{MockMvcOperationRequestFactory.java => MockMvcRequestConverter.java} (83%) rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{MockMvcOperationResponseFactory.java => MockMvcResponseConverter.java} (77%) rename spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/{MockMvcOperationRequestFactoryTests.java => MockMvcRequestConverterTests.java} (92%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/{RestAssuredOperationRequestFactory.java => RestAssuredRequestConverter.java} (92%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/{RestAssuredOperationResponseFactory.java => RestAssuredResponseConverter.java} (84%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/{RestAssuredOperationRequestFactoryTests.java => RestAssuredRequestConverterTests.java} (85%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java new file mode 100644 index 000000000..5bdf27c13 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2016 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; + +/** + * An exception that can be thrown when a failure occurs during REST documentation + * generation. + * + * @author Andy Wilkinson + */ +public class RestDocumentationException extends RuntimeException { + + /** + * Creates a new {@code RestDocumentationException} with the given {@code cause}. + * + * @param cause the cause + */ + public RestDocumentationException(Throwable cause) { + super(cause); + } + + /** + * Creates a new {@code RestDocumentationException} with the given {@code message} and + * {@code cause}. + * + * @param message the message + * @param cause the cause + */ + public RestDocumentationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java new file mode 100644 index 000000000..f8be483b8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-2016 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.Assert; + +/** + * A {@code RestDocumentationHandler} is used to produce documentation snippets from the + * request and response of an operation performed on a service. + * + * @param the request type that can be handled + * @param the response type that can be handled + * @author Andy Wilkinson + */ +public final class RestDocumentationHandler { + + private final String identifier; + + private final OperationRequestPreprocessor requestPreprocessor; + + private final OperationResponsePreprocessor responsePreprocessor; + + private final List snippets; + + private final RequestConverter requestConverter; + + private final ResponseConverter responseConverter; + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given documentation {@code snippets} will be produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, + new IdentityOperationRequestPreprocessor(), + new IdentityOperationResponsePreprocessor(), snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code requestPreprocessor} is applied to the + * request before it is documented. The given documentation {@code snippets} will be + * produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, requestPreprocessor, + new IdentityOperationResponsePreprocessor(), snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code responsePreprocessor} is applied to the + * response before it is documented. The given documentation {@code snippets} will be + * produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, + new IdentityOperationRequestPreprocessor(), responsePreprocessor, + snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code requestPreprocessor} and + * {@code responsePreprocessor} are applied to the request and response before they + * are documented. The given documentation {@code snippets} will be produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + Assert.notNull(identifier, "identifier must be non-null"); + Assert.notNull(requestConverter, "requestConverter must be non-null"); + Assert.notNull(responseConverter, "responseConverter must be non-null"); + Assert.notNull(identifier, "identifier must be non-null"); + Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null"); + Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null"); + Assert.notNull(snippets, "snippets must be non-null"); + this.identifier = identifier; + this.requestConverter = requestConverter; + this.responseConverter = responseConverter; + this.requestPreprocessor = requestPreprocessor; + this.responsePreprocessor = responsePreprocessor; + this.snippets = new ArrayList<>(Arrays.asList(snippets)); + } + + /** + * Handles the given {@code request} and {@code response}, producing documentation + * snippets for them using the given {@code configuration}. + * + * @param request the request + * @param response the request + * @param configuration the configuration + * @throws RestDocumentationException if a failure occurs during handling + */ + public void handle(REQ request, RESP response, Map configuration) { + OperationRequest operationRequest = this.requestPreprocessor + .preprocess(this.requestConverter.convert(request)); + + OperationResponse operationResponse = this.responsePreprocessor + .preprocess(this.responseConverter.convert(response)); + Map attributes = new HashMap<>(configuration); + Operation operation = new StandardOperation(this.identifier, operationRequest, + operationResponse, attributes); + try { + for (Snippet snippet : getSnippets(attributes)) { + snippet.document(operation); + } + } + catch (IOException ex) { + throw new RestDocumentationException(ex); + } + } + + /** + * Adds the given {@code snippets} such that they are documented when this handler is + * called. + * + * @param snippets the snippets to add + */ + public void addSnippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + } + + @SuppressWarnings("unchecked") + private List getSnippets(Map configuration) { + List combinedSnippets = new ArrayList<>(this.snippets); + List defaultSnippets = (List) configuration + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS); + if (defaultSnippets != null) { + combinedSnippets.addAll(defaultSnippets); + } + return combinedSnippets; + } + + private static final class IdentityOperationRequestPreprocessor implements + OperationRequestPreprocessor { + + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + } + + private static final class IdentityOperationResponsePreprocessor implements + OperationResponsePreprocessor { + + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java new file mode 100644 index 000000000..397f79d9e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * An exception that can be thrown by {@link RequestConverter} and + * {@link ResponseConverter} implementations to indicate that a failure has occurred + * during conversion. + * + * @author Andy Wilkinson + * @see RequestConverter#convert(Object) + * @see ResponseConverter#convert(Object) + */ +public class ConversionException extends RuntimeException { + + /** + * Creates a new {@code ConversionException} with the given {@code cause}. + * + * @param cause the cause + */ + public ConversionException(Throwable cause) { + super(cause); + } + + /** + * Creates a new {@code ConversionException} with the given {@code message} and + * {@code cause}. + * + * @param message the message + * @param cause the cause + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java new file mode 100644 index 000000000..5c2377d5a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * A {@code RequestConverter} is used to convert an implementation-specific request into + * an {@link OperationRequest}. + * + * @param The implementation-specific request type + * @author Andy Wilkinson + */ +public interface RequestConverter { + + /** + * Converts the given {@code request} into an {@code OperationRequest}. + * + * @param request the request + * @return the operation request + * @throws ConversionException if the conversion fails + */ + OperationRequest convert(R request); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java new file mode 100644 index 000000000..48a76be28 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * A {@code ResponseConverter} is used to convert an implementation-specific response into + * an {@link OperationResponse}. + * + * @param The implementation-specific response type + * @author Andy Wilkinson + */ +public interface ResponseConverter { + + /** + * Converts the given {@code response} into an {@code OperationResponse}. + * + * @param response the response + * @return the operation response + * @throws ConversionException if the conversion fails + */ + OperationResponse convert(R response); + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java new file mode 100644 index 000000000..abfdafa55 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2016 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; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.snippet.Snippet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestDocumentationHandler}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationHandlerTests { + + @SuppressWarnings("unchecked") + private final RequestConverter requestConverter = mock(RequestConverter.class); + + @SuppressWarnings("unchecked") + private final ResponseConverter responseConverter = mock(ResponseConverter.class); + + private final Object request = new Object(); + + private final Object response = new Object(); + + private final OperationRequest operationRequest = new OperationRequestFactory() + .create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), + null, null); + + private final OperationResponse operationResponse = new OperationResponseFactory() + .create(null, null, null); + + private final Snippet snippet = mock(Snippet.class); + + @Test + public void basicHandling() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + HashMap configuration = new HashMap<>(); + new RestDocumentationHandler<>("id", this.requestConverter, + this.responseConverter, this.snippet).handle(this.request, this.response, + configuration); + verifySnippetInvocation(this.snippet, configuration); + } + + @Test + public void defaultSnippetsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + HashMap configuration = new HashMap<>(); + Snippet defaultSnippet1 = mock(Snippet.class); + Snippet defaultSnippet2 = mock(Snippet.class); + configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, + Arrays.asList(defaultSnippet1, defaultSnippet2)); + new RestDocumentationHandler<>("id", this.requestConverter, + this.responseConverter, this.snippet).handle(this.request, this.response, + configuration); + verifySnippetInvocation(this.snippet, configuration); + verifySnippetInvocation(defaultSnippet1, configuration); + verifySnippetInvocation(defaultSnippet2, configuration); + } + + @Test + public void additionalSnippetsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + Snippet additionalSnippet1 = mock(Snippet.class); + Snippet additionalSnippet2 = mock(Snippet.class); + RestDocumentationHandler handler = new RestDocumentationHandler<>( + "id", this.requestConverter, this.responseConverter, this.snippet); + handler.addSnippets(additionalSnippet1, additionalSnippet2); + HashMap configuration = new HashMap<>(); + handler.handle(this.request, this.response, configuration); + verifySnippetInvocation(this.snippet, configuration); + verifySnippetInvocation(additionalSnippet1, configuration); + verifySnippetInvocation(additionalSnippet2, configuration); + } + + private void verifySnippetInvocation(Snippet snippet, Map attributes) + throws IOException { + ArgumentCaptor operation = ArgumentCaptor.forClass(Operation.class); + verify(snippet).document(operation.capture()); + assertThat(this.operationRequest, is(equalTo(operation.getValue().getRequest()))); + assertThat(this.operationResponse, + is(equalTo(operation.getValue().getResponse()))); + assertThat(attributes, is(equalTo(operation.getValue().getAttributes()))); + } +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java similarity index 83% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 9449354a8..6ad0c5531 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -32,11 +32,13 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.restdocs.operation.ConversionException; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestConverter; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -44,13 +46,13 @@ import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; /** - * A factory for creating an {@link OperationRequest} from a + * A converter for creating an {@link OperationRequest} from a * {@link MockHttpServletRequest}. * * @author Andy Wilkinson * */ -class MockMvcOperationRequestFactory { +class MockMvcRequestConverter implements RequestConverter { private static final String SCHEME_HTTP = "http"; @@ -60,28 +62,29 @@ class MockMvcOperationRequestFactory { private static final int STANDARD_PORT_HTTPS = 443; - /** - * Creates a new {@code OperationRequest} derived from the given {@code mockRequest}. - * - * @param mockRequest the request - * @return the {@code OperationRequest} - * @throws Exception if the request could not be created - */ - OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) - throws Exception { - HttpHeaders headers = extractHeaders(mockRequest); - Parameters parameters = extractParameters(mockRequest); - List parts = extractParts(mockRequest); - String queryString = mockRequest.getQueryString(); - if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { - queryString = parameters.toQueryString(); + @Override + public OperationRequest convert(MockHttpServletRequest mockRequest) { + try { + HttpHeaders headers = extractHeaders(mockRequest); + Parameters parameters = extractParameters(mockRequest); + List parts = extractParts(mockRequest); + String queryString = mockRequest.getQueryString(); + if (!StringUtils.hasText(queryString) + && "GET".equals(mockRequest.getMethod())) { + queryString = parameters.toQueryString(); + } + return new OperationRequestFactory() + .create(URI + .create(getRequestUri(mockRequest) + + (StringUtils.hasText(queryString) ? "?" + + queryString : "")), + HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils + .copyToByteArray(mockRequest.getInputStream()), + headers, parameters, parts); + } + catch (Exception ex) { + throw new ConversionException(ex); } - return new OperationRequestFactory().create( - URI.create(getRequestUri(mockRequest) - + (StringUtils.hasText(queryString) ? "?" + queryString : "")), - HttpMethod.valueOf(mockRequest.getMethod()), - FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, - parameters, parts); } private List extractParts(MockHttpServletRequest servletRequest) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java similarity index 77% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index 4e4f765f5..d0677cce9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -21,22 +21,18 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; /** - * A factory for creating an {@link OperationResponse} derived from a + * A converter for creating an {@link OperationResponse} derived from a * {@link MockHttpServletResponse}. * * @author Andy Wilkinson */ -class MockMvcOperationResponseFactory { +class MockMvcResponseConverter implements ResponseConverter { - /** - * Create a new {@code OperationResponse} derived from the given {@code mockResponse}. - * - * @param mockResponse the response - * @return the {@code OperationResponse} - */ - OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { + @Override + public OperationResponse convert(MockHttpServletResponse mockResponse) { return new OperationResponseFactory().create( HttpStatus.valueOf(mockResponse.getStatus()), extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); @@ -51,4 +47,5 @@ private HttpHeaders extractHeaders(MockHttpServletResponse response) { } return headers; } + } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 541f380d9..64825f7ea 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -32,6 +33,10 @@ */ public abstract class MockMvcRestDocumentation { + private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); + + private static final MockMvcResponseConverter RESPONSE_CONVERTER = new MockMvcResponseConverter(); + private MockMvcRestDocumentation() { } @@ -61,7 +66,8 @@ public static MockMvcRestDocumentationConfigurer documentationConfiguration( */ public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** @@ -78,8 +84,9 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, requestPreprocessor, - snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + snippets)); } /** @@ -96,8 +103,9 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, responsePreprocessor, - snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, + snippets)); } /** @@ -117,8 +125,9 @@ public static RestDocumentationResultHandler document(String identifier, public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, requestPreprocessor, - responsePreprocessor, snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + responsePreprocessor, snippets)); } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index a402487fe..7921cab8c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -88,12 +88,12 @@ private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentati @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { RestDocumentationContext context = this.restDocumentation.beforeOperation(); - request.setAttribute(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; configuration.put(urlTemplateAttribute, request.getAttribute(urlTemplateAttribute)); + configuration.put(RestDocumentationContext.class.getName(), context); request.setAttribute("org.springframework.restdocs.configuration", configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 075c162fb..3194e826e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,20 +16,11 @@ package org.springframework.restdocs.mockmvc; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.SnippetConfigurer; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -44,68 +35,20 @@ */ public class RestDocumentationResultHandler implements ResultHandler { - private final String identifier; + private final RestDocumentationHandler delegate; - private final OperationRequestPreprocessor requestPreprocessor; - - private final OperationResponsePreprocessor responsePreprocessor; - - private final List snippets; - - RestDocumentationResultHandler(String identifier, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - this(identifier, requestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - responsePreprocessor, snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - Assert.notNull(identifier, "identifier must be non-null"); - Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null"); - Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null"); - Assert.notNull(snippets, "snippets must be non-null"); - this.identifier = identifier; - this.requestPreprocessor = requestPreprocessor; - this.responsePreprocessor = responsePreprocessor; - this.snippets = new ArrayList<>(Arrays.asList(snippets)); + RestDocumentationResultHandler( + RestDocumentationHandler delegate) { + Assert.notNull(delegate, "delegate must be non-null"); + this.delegate = delegate; } @Override public void handle(MvcResult result) throws Exception { - Map attributes = new HashMap<>(); - attributes.put(RestDocumentationContext.class.getName(), result.getRequest() - .getAttribute(RestDocumentationContext.class.getName())); - attributes.put("org.springframework.restdocs.urlTemplate", result.getRequest() - .getAttribute("org.springframework.restdocs.urlTemplate")); @SuppressWarnings("unchecked") Map configuration = (Map) result.getRequest() .getAttribute("org.springframework.restdocs.configuration"); - attributes.putAll(configuration); - - OperationRequest request = this.requestPreprocessor - .preprocess(new MockMvcOperationRequestFactory() - .createOperationRequest(result.getRequest())); - - OperationResponse response = this.responsePreprocessor - .preprocess(new MockMvcOperationResponseFactory() - .createOperationResponse(result.getResponse())); - Operation operation = new StandardOperation(this.identifier, request, response, - attributes); - for (Snippet snippet : getSnippets(result)) { - snippet.document(operation); - } + this.delegate.handle(result.getRequest(), result.getResponse(), configuration); } /** @@ -113,41 +56,11 @@ public void handle(MvcResult result) throws Exception { * handler is called. * * @param snippets the snippets to add - * @return this {@code ResultDocumentationResultHandler} + * @return this {@code RestDocumentationResultHandler} */ public RestDocumentationResultHandler snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.delegate.addSnippets(snippets); return this; } - @SuppressWarnings("unchecked") - private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>( - (List) ((Map) result.getRequest().getAttribute( - "org.springframework.restdocs.configuration")) - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); - combinedSnippets.addAll(this.snippets); - return combinedSnippets; - } - - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { - - @Override - public OperationRequest preprocess(OperationRequest request) { - return request; - } - - } - - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { - - @Override - public OperationResponse preprocess(OperationResponse response) { - return response; - } - - } - } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java similarity index 92% rename from spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java rename to spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 6770d31bb..5c5c2aeaf 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -43,13 +43,13 @@ import static org.mockito.Mockito.mock; /** - * Tests for {@link MockMvcOperationRequestFactory}. + * Tests for {@link MockMvcRequestConverter}. * * @author Andy Wilkinson */ -public class MockMvcOperationRequestFactoryTests { +public class MockMvcRequestConverterTests { - private final MockMvcOperationRequestFactory factory = new MockMvcOperationRequestFactory(); + private final MockMvcRequestConverter factory = new MockMvcRequestConverter(); @Test public void httpRequest() throws Exception { @@ -64,7 +64,7 @@ public void httpRequestWithCustomPort() throws Exception { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") .buildRequest(new MockServletContext()); mockRequest.setServerPort(8080); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -93,7 +93,7 @@ public void httpsRequest() throws Exception { .buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(443); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("https://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -104,7 +104,7 @@ public void httpsRequestWithCustomPort() throws Exception { .buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(8443); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -191,7 +191,7 @@ public void requestWithPart() throws Exception { given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.txt"); mockRequest.addPart(mockPart); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getParts().size(), is(1)); OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("part-name"))); @@ -216,7 +216,7 @@ public void requestWithPartWithContentType() throws Exception { given(mockPart.getSubmittedFileName()).willReturn("submitted.png"); given(mockPart.getContentType()).willReturn("image/png"); mockRequest.addPart(mockPart); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getParts().size(), is(1)); OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("part-name"))); @@ -229,8 +229,7 @@ public void requestWithPartWithContentType() throws Exception { private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) throws Exception { - return this.factory.createOperationRequest(builder - .buildRequest(new MockServletContext())); + return this.factory.convert(builder.buildRequest(new MockServletContext())); } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java similarity index 92% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index c3ef0b20c..cfc19677d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -30,20 +30,23 @@ import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestConverter; import com.jayway.restassured.response.Header; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.MultiPartSpecification; /** - * A factory for creating an {@link OperationRequest} derived from a REST Assured + * A converter for creating an {@link OperationRequest} from a REST Assured * {@link FilterableRequestSpecification}. * * @author Andy Wilkinson */ -class RestAssuredOperationRequestFactory { +class RestAssuredRequestConverter implements + RequestConverter { - OperationRequest createOperationRequest(FilterableRequestSpecification requestSpec) { + @Override + public OperationRequest convert(FilterableRequestSpecification requestSpec) { return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), HttpMethod.valueOf(requestSpec.getMethod().name()), extractContent(requestSpec), extractHeaders(requestSpec), diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java similarity index 84% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index bbcd251ee..425fc96fb 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -20,19 +20,21 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; import com.jayway.restassured.response.Header; import com.jayway.restassured.response.Response; /** - * A factory for creating an {@link OperationResponse} derived from a REST Assured + * A converter for creating an {@link OperationResponse} from a REST Assured * {@link Response}. * * @author Andy Wilkinson */ -class RestAssuredOperationResponseFactory { +class RestAssuredResponseConverter implements ResponseConverter { - OperationResponse createOperationResponse(Response response) { + @Override + public OperationResponse convert(Response response) { return new OperationResponseFactory().create( HttpStatus.valueOf(response.getStatusCode()), extractHeaders(response), extractContent(response)); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 447ba1f55..0e47cbd24 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.restassured; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -28,6 +29,10 @@ */ public abstract class RestAssuredRestDocumentation { + private static final RestAssuredRequestConverter REQUEST_CONVERTER = new RestAssuredRequestConverter(); + + private static final RestAssuredResponseConverter RESPONSE_CONVERTER = new RestAssuredResponseConverter(); + private RestAssuredRestDocumentation() { } @@ -41,7 +46,8 @@ private RestAssuredRestDocumentation() { * @return a {@link RestDocumentationFilter} that will produce the documentation */ public static RestDocumentationFilter document(String identifier, Snippet... snippets) { - return new RestDocumentationFilter(identifier, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** @@ -56,7 +62,8 @@ public static RestDocumentationFilter document(String identifier, Snippet... sni */ public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, requestPreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } /** @@ -71,7 +78,8 @@ public static RestDocumentationFilter document(String identifier, */ public static RestDocumentationFilter document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, responsePreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } /** @@ -89,8 +97,9 @@ public static RestDocumentationFilter document(String identifier, public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, requestPreprocessor, - responsePreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + responsePreprocessor, snippets)); } /** diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 3460a99a3..f588ed168 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -16,22 +16,13 @@ package org.springframework.restdocs.restassured; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.SnippetConfigurer; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.Assert; import com.jayway.restassured.filter.Filter; import com.jayway.restassured.filter.FilterContext; @@ -46,40 +37,12 @@ */ public final class RestDocumentationFilter implements Filter { - private final String identifier; + private final RestDocumentationHandler delegate; - private final OperationRequestPreprocessor requestPreprocessor; - - private final OperationResponsePreprocessor responsePreprocessor; - - private final List snippets; - - RestDocumentationFilter(String identifier, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationFilter(String identifier, - OperationRequestPreprocessor operationRequestPreprocessor, - Snippet... snippets) { - this(identifier, operationRequestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationFilter(String identifier, - OperationResponsePreprocessor operationResponsePreprocessor, - Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - operationResponsePreprocessor, snippets); - } - - RestDocumentationFilter(String identifier, - OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this.identifier = identifier; - this.requestPreprocessor = requestPreprocessor; - this.responsePreprocessor = responsePreprocessor; - this.snippets = new ArrayList<>(Arrays.asList(snippets)); + RestDocumentationFilter( + RestDocumentationHandler delegate) { + Assert.notNull(delegate, "delegate must be non-null"); + this.delegate = delegate; } @Override @@ -87,35 +50,15 @@ public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext context) { Response response = context.next(requestSpec, responseSpec); - OperationRequest operationRequest = this.requestPreprocessor - .preprocess(new RestAssuredOperationRequestFactory() - .createOperationRequest(requestSpec)); - OperationResponse operationResponse = this.responsePreprocessor - .preprocess(new RestAssuredOperationResponseFactory() - .createOperationResponse(response)); - - RestDocumentationContext documentationContext = context - .getValue(RestDocumentationContext.class.getName()); - - Map attributes = new HashMap<>(); - attributes.put(RestDocumentationContext.class.getName(), documentationContext); - attributes.put("org.springframework.restdocs.urlTemplate", + Map configuration = new HashMap<>( + context.>getValue("org.springframework.restdocs.configuration")); + configuration.put(RestDocumentationContext.class.getName(), context + .getValue(RestDocumentationContext.class + .getName())); + configuration.put("org.springframework.restdocs.urlTemplate", requestSpec.getUserDefinedPath()); - Map configuration = context - .getValue("org.springframework.restdocs.configuration"); - attributes.putAll(configuration); - Operation operation = new StandardOperation(this.identifier, operationRequest, - operationResponse, attributes); - - try { - for (Snippet snippet : getSnippets(configuration)) { - snippet.document(operation); - } - } - catch (IOException ex) { - throw new RuntimeException(ex); - } + this.delegate.handle(requestSpec, response, configuration); return response; } @@ -128,37 +71,8 @@ public Response filter(FilterableRequestSpecification requestSpec, * @return this {@code RestDocumentationFilter} */ public RestDocumentationFilter snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.delegate.addSnippets(snippets); return this; } - @SuppressWarnings("unchecked") - private List getSnippets(Map configuration) { - List combinedSnippets = new ArrayList<>( - (List) configuration - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); - combinedSnippets.addAll(this.snippets); - return combinedSnippets; - } - - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { - - @Override - public OperationRequest preprocess(OperationRequest request) { - return request; - } - - } - - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { - - @Override - public OperationResponse preprocess(OperationResponse response) { - return response; - } - - } - } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java similarity index 85% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index bb5325598..f39b15b2b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -36,7 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredOperationRequestFactoryTests.TestApplication; +import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.bind.annotation.RequestMapping; @@ -51,7 +51,7 @@ import static org.junit.Assert.assertThat; /** - * Tests for {@link RestAssuredOperationRequestFactory}. + * Tests for {@link RestAssuredRequestConverter}. * * @author Andy Wilkinson */ @@ -59,12 +59,12 @@ @SpringApplicationConfiguration(classes = TestApplication.class) @WebAppConfiguration @IntegrationTest("server.port=0") -public class RestAssuredOperationRequestFactoryTests { +public class RestAssuredRequestConverterTests { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final RestAssuredOperationRequestFactory factory = new RestAssuredOperationRequestFactory(); + private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); @Value("${local.server.port}") private int port; @@ -74,7 +74,7 @@ public void requestUri() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.get("/foo/bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getUri(), is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); } @@ -84,7 +84,7 @@ public void requestMethod() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.head("/foo/bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getMethod(), is(equalTo(HttpMethod.HEAD))); } @@ -94,7 +94,7 @@ public void queryStringParameters() { .queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -104,7 +104,7 @@ public void queryStringFromUrlParameters() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.get("/?foo=bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -115,7 +115,7 @@ public void formParameters() { .formParameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -126,7 +126,7 @@ public void requestParameters() { .parameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -137,7 +137,7 @@ public void headers() { .header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getHeaders().toString(), request.getHeaders().size(), is(3)); assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); @@ -152,7 +152,7 @@ public void multipart() { .multiPart("b", new ObjectBody("bar"), "application/json"); requestSpec.post().then().statusCode(200); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); Collection parts = request.getParts(); assertThat(parts.size(), is(2)); Iterator iterator = parts.iterator(); @@ -174,7 +174,7 @@ public void byteArrayBody() { RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) .port(this.port); requestSpec.post(); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test @@ -183,7 +183,7 @@ public void stringBody() { .port(this.port); requestSpec.post(); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getContentAsString(), is(equalTo("body"))); } @@ -193,7 +193,7 @@ public void objectBody() { .body(new ObjectBody("bar")).port(this.port); requestSpec.post(); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); } @@ -205,7 +205,7 @@ public void inputStreamBodyIsNotSupported() { requestSpec.post(); this.thrown .expectMessage(equalTo("Unsupported request content: java.io.ByteArrayInputStream")); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test @@ -214,7 +214,7 @@ public void fileBodyIsNotSupported() { .body(new File("src/test/resources/body.txt")).port(this.port); requestSpec.post(); this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } /** From 1897ec54f202f35d0aab2b0b1a39b734374dd4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Thu, 4 Feb 2016 11:55:03 +0100 Subject: [PATCH 0214/1059] Add support for removing headers that match a Pattern A new preprocessor method `removeMatchingHeaders` has been introduced which takes one or more regular expression patterns (as Strings). Any header that matches a pattern is removed. Closes gh-195 --- .../customizing-requests-and-responses.adoc | 2 + .../HeaderRemovingOperationPreprocessor.java | 40 +++++++++++++++---- .../operation/preprocess/Preprocessors.java | 17 +++++++- ...derRemovingOperationPreprocessorTests.java | 32 +++++++++++++-- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 900106388..6011df8d7 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -105,6 +105,8 @@ different replacement can also be specified if you wish. `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. +`removeMatchingHeaders` on `Preprocessors` applies the given patterns on every header and +removes them when matching. [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 7766af57c..689acc8ff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; @@ -27,20 +28,28 @@ import org.springframework.restdocs.operation.OperationResponseFactory; /** - * An {@link OperationPreprocessor} that removes headers. + * An {@link OperationPreprocessor} that removes headers. The headers to remove are + * provided as constructor arguments and can be either plain string or patterns to match + * against the headers found * * @author Andy Wilkinson */ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - private final Set headersToRemove; + private final Set plainHeadersToRemove; + private final Set patternHeadersToRemove; + + HeaderRemovingOperationPreprocessor(String ... headersToRemove) { + this.plainHeadersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + this.patternHeadersToRemove = null; + } - HeaderRemovingOperationPreprocessor(String... headersToRemove) { - this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + HeaderRemovingOperationPreprocessor(Pattern ... patternHeadersToRemove) { + this.plainHeadersToRemove = null; + this.patternHeadersToRemove = new HashSet<>(Arrays.asList(patternHeadersToRemove)); } @Override @@ -58,8 +67,25 @@ public OperationRequest preprocess(OperationRequest request) { private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { HttpHeaders processedHeaders = new HttpHeaders(); processedHeaders.putAll(originalHeaders); - for (String headerToRemove : this.headersToRemove) { - processedHeaders.remove(headerToRemove); + if (this.plainHeadersToRemove != null) { + for (String headerToRemove : this.plainHeadersToRemove) { + processedHeaders.remove(headerToRemove); + } + } + else { + Set toRemove = new HashSet<>(); + for (String headerToCheck : originalHeaders.keySet()) { + for (Pattern pattern : this.patternHeadersToRemove) { + if (pattern.matcher(headerToCheck).matches()) { + toRemove.add(headerToCheck); + } + } + } + // Remove afterwards to avoid side effects when removing while iterating over + // the set keys : + for (String headerToRemove : toRemove) { + processedHeaders.remove(headerToRemove); + } } return processedHeaders; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index b4aa590dd..bacb4fd73 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -76,13 +76,28 @@ public static OperationPreprocessor prettyPrint() { * Returns an {@code OperationPreprocessor} that will remove headers from the request * or response. * - * @param headersToRemove the names of the headers to remove + * @param headersToRemove the names of the headers to remove. * @return the preprocessor */ public static OperationPreprocessor removeHeaders(String... headersToRemove) { return new HeaderRemovingOperationPreprocessor(headersToRemove); } + /** + * Returns an {@code OperationPreprocessor} that will remove headers from the request + * or response based on a pattern match. + * + * @param headerPatternsToRemove pattern for the header names to remove. Every matchig header will be removed. + * @return the preprocessor + */ + public static OperationPreprocessor removeMatchingHeaders(String... headerPatternsToRemove) { + Pattern[] patterns = new Pattern[headerPatternsToRemove.length]; + for (int i = 0; i < headerPatternsToRemove.length; i++) { + patterns[i] = Pattern.compile(headerPatternsToRemove[i]); + } + return new HeaderRemovingOperationPreprocessor(patterns); + } + /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 634ef6162..99a661354 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -19,6 +19,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; +import java.util.regex.Pattern; import org.junit.Test; import org.springframework.http.HttpHeaders; @@ -66,18 +67,43 @@ public void modifyRequestHeaders() { @Test public void modifyResponseHeaders() { - OperationResponse response = this.responseFactory.create(HttpStatus.OK, - getHttpHeaders(), new byte[0]); + OperationResponse response = createResponse(); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); } - private HttpHeaders getHttpHeaders() { + @Test + public void modifyWithPattern() { + OperationResponse response = createResponse("content-length", "1234"); + HeaderRemovingOperationPreprocessor processor = + new HeaderRemovingOperationPreprocessor(Pattern.compile("co.*le(.)gth]")); + OperationResponse preprocessed = processor.preprocess(response); + assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); + assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + assertThat(preprocessed.getHeaders(), hasEntry("b", Arrays.asList("bravo", "banana"))); + } + + @Test + public void removeAllHeaders() { + HeaderRemovingOperationPreprocessor processor = + new HeaderRemovingOperationPreprocessor(Pattern.compile(".*")); + OperationResponse preprocessed = processor.preprocess(createResponse()); + assertThat(preprocessed.getHeaders().size(), is(equalTo(0))); + } + + private OperationResponse createResponse(String ... extraHeaders) { + return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), new byte[0]); + } + + private HttpHeaders getHttpHeaders(String ... extraHeaders) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("a", "alpha"); httpHeaders.add("b", "bravo"); httpHeaders.add("b", "banana"); + for (int i = 0; i < extraHeaders.length; i += 2) { + httpHeaders.add(extraHeaders[i], extraHeaders[i + 1]); + } return httpHeaders; } From 1131c9fa6ee7137f6b85dbbf183b39eb3321dec8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 09:32:14 +0000 Subject: [PATCH 0215/1059] Polish pattern-based header removal contribution Closes gh-191 --- .../customizing-requests-and-responses.adoc | 8 +-- .../preprocess/ExactMatchHeaderFilter.java | 41 +++++++++++++++ .../operation/preprocess/HeaderFilter.java | 35 +++++++++++++ .../HeaderRemovingOperationPreprocessor.java | 43 ++++------------ .../preprocess/PatternMatchHeaderFilter.java | 50 +++++++++++++++++++ .../operation/preprocess/Preprocessors.java | 32 ++++++------ ...derRemovingOperationPreprocessorTests.java | 25 +++++----- 7 files changed, 171 insertions(+), 63 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 6011df8d7..8cb020544 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -102,11 +102,11 @@ different replacement can also be specified if you wish. [[customizing-requests-and-responses-preprocessors-remove-headers]] ==== Removing headers -`removeHeaders` on `Preprocessors` removes any occurrences of the named headers -from the request or response. +`removeHeaders` on `Preprocessors` removes any headers from the request or response where +the name is equal to any of the given header names. -`removeMatchingHeaders` on `Preprocessors` applies the given patterns on every header and -removes them when matching. +`removeMatchingHeaders` on `Preprocessors` removes any headers from the request or +response where the name matches any of the given regular expression patterns. [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java new file mode 100644 index 000000000..6e1cd8731 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link HeaderFilter} that excludes a header if its name is an exact match. + * + * @author Andy Wilkinson + */ +class ExactMatchHeaderFilter implements HeaderFilter { + + private final Set headersToExclude; + + ExactMatchHeaderFilter(String... headersToExclude) { + this.headersToExclude = new HashSet<>(Arrays.asList(headersToExclude)); + } + + @Override + public boolean excludeHeader(String name) { + return this.headersToExclude.contains(name); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java new file mode 100644 index 000000000..6b14f2494 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +/** + * A strategy for determining whether or not a header should be excluded. + * + * @author Andy Wilkinson + */ +interface HeaderFilter { + + /** + * Called to determine whether a header should be excluded. Return {@code true} to + * exclude a header, otherwise {@code false}. + * + * @param name the name of the header + * @return {@code true} to exclude the header, otherwise {@code false} + */ + boolean excludeHeader(String name); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 689acc8ff..749f61055 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,10 +16,7 @@ package org.springframework.restdocs.operation.preprocess; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Pattern; +import java.util.Iterator; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; @@ -37,19 +34,13 @@ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - private final Set plainHeadersToRemove; - private final Set patternHeadersToRemove; + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - HeaderRemovingOperationPreprocessor(String ... headersToRemove) { - this.plainHeadersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); - this.patternHeadersToRemove = null; - } + private final HeaderFilter headerFilter; - HeaderRemovingOperationPreprocessor(Pattern ... patternHeadersToRemove) { - this.plainHeadersToRemove = null; - this.patternHeadersToRemove = new HashSet<>(Arrays.asList(patternHeadersToRemove)); + HeaderRemovingOperationPreprocessor(HeaderFilter headerFilter) { + this.headerFilter = headerFilter; } @Override @@ -67,24 +58,10 @@ public OperationRequest preprocess(OperationRequest request) { private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { HttpHeaders processedHeaders = new HttpHeaders(); processedHeaders.putAll(originalHeaders); - if (this.plainHeadersToRemove != null) { - for (String headerToRemove : this.plainHeadersToRemove) { - processedHeaders.remove(headerToRemove); - } - } - else { - Set toRemove = new HashSet<>(); - for (String headerToCheck : originalHeaders.keySet()) { - for (Pattern pattern : this.patternHeadersToRemove) { - if (pattern.matcher(headerToCheck).matches()) { - toRemove.add(headerToCheck); - } - } - } - // Remove afterwards to avoid side effects when removing while iterating over - // the set keys : - for (String headerToRemove : toRemove) { - processedHeaders.remove(headerToRemove); + Iterator headers = processedHeaders.keySet().iterator(); + while (headers.hasNext()) { + if (this.headerFilter.excludeHeader(headers.next())) { + headers.remove(); } } return processedHeaders; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java new file mode 100644 index 000000000..6fff33865 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A {@link HeaderFilter} that excludes a header if its name matches a {@link Pattern}. + * + * @author Andy Wilkinson + * @author Roland Huss + */ +class PatternMatchHeaderFilter implements HeaderFilter { + + private Set exclusionPatterns; + + PatternMatchHeaderFilter(String... exclusionPatterns) { + this.exclusionPatterns = new HashSet<>(); + for (String exclusionPattern : exclusionPatterns) { + this.exclusionPatterns.add(Pattern.compile(exclusionPattern)); + } + } + + @Override + public boolean excludeHeader(String name) { + for (Pattern pattern : this.exclusionPatterns) { + if (pattern.matcher(name).matches()) { + return true; + } + } + return false; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index bacb4fd73..a0bae926d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -30,6 +30,7 @@ * documented. * * @author Andy Wilkinson + * @author Roland Huss */ public final class Preprocessors { @@ -73,29 +74,32 @@ public static OperationPreprocessor prettyPrint() { } /** - * Returns an {@code OperationPreprocessor} that will remove headers from the request - * or response. + * Returns an {@code OperationPreprocessor} that will remove any header from the + * request or response with a name that is equal to one of the given + * {@code headersToRemove}. * - * @param headersToRemove the names of the headers to remove. + * @param headerNames the header names * @return the preprocessor + * @see String#equals(Object) */ - public static OperationPreprocessor removeHeaders(String... headersToRemove) { - return new HeaderRemovingOperationPreprocessor(headersToRemove); + public static OperationPreprocessor removeHeaders(String... headerNames) { + return new HeaderRemovingOperationPreprocessor(new ExactMatchHeaderFilter( + headerNames)); } /** - * Returns an {@code OperationPreprocessor} that will remove headers from the request - * or response based on a pattern match. + * Returns an {@code OperationPreprocessor} that will remove any headers from the + * request or response with a name that matches one of the given + * {@code headerNamePatterns} regular expressions. * - * @param headerPatternsToRemove pattern for the header names to remove. Every matchig header will be removed. + * @param headerNamePatterns the header name patterns * @return the preprocessor + * @see java.util.regex.Matcher#matches() */ - public static OperationPreprocessor removeMatchingHeaders(String... headerPatternsToRemove) { - Pattern[] patterns = new Pattern[headerPatternsToRemove.length]; - for (int i = 0; i < headerPatternsToRemove.length; i++) { - patterns[i] = Pattern.compile(headerPatternsToRemove[i]); - } - return new HeaderRemovingOperationPreprocessor(patterns); + public static OperationPreprocessor removeMatchingHeaders( + String... headerNamePatterns) { + return new HeaderRemovingOperationPreprocessor(new PatternMatchHeaderFilter( + headerNamePatterns)); } /** diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 99a661354..a0e5c3804 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,7 +19,6 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; -import java.util.regex.Pattern; import org.junit.Test; import org.springframework.http.HttpHeaders; @@ -41,7 +40,7 @@ * Tests for {@link HeaderRemovingOperationPreprocessorTests}. * * @author Andy Wilkinson - * + * @author Roland Huss */ public class HeaderRemovingOperationPreprocessorTests { @@ -50,7 +49,7 @@ public class HeaderRemovingOperationPreprocessorTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( - "b"); + new ExactMatchHeaderFilter("b")); @Test public void modifyRequestHeaders() { @@ -76,27 +75,29 @@ public void modifyResponseHeaders() { @Test public void modifyWithPattern() { OperationResponse response = createResponse("content-length", "1234"); - HeaderRemovingOperationPreprocessor processor = - new HeaderRemovingOperationPreprocessor(Pattern.compile("co.*le(.)gth]")); + HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( + new PatternMatchHeaderFilter("co.*le(.)gth]")); OperationResponse preprocessed = processor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); - assertThat(preprocessed.getHeaders(), hasEntry("b", Arrays.asList("bravo", "banana"))); + assertThat(preprocessed.getHeaders(), + hasEntry("b", Arrays.asList("bravo", "banana"))); } @Test public void removeAllHeaders() { - HeaderRemovingOperationPreprocessor processor = - new HeaderRemovingOperationPreprocessor(Pattern.compile(".*")); + HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( + new PatternMatchHeaderFilter(".*")); OperationResponse preprocessed = processor.preprocess(createResponse()); assertThat(preprocessed.getHeaders().size(), is(equalTo(0))); } - private OperationResponse createResponse(String ... extraHeaders) { - return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), new byte[0]); + private OperationResponse createResponse(String... extraHeaders) { + return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), + new byte[0]); } - private HttpHeaders getHttpHeaders(String ... extraHeaders) { + private HttpHeaders getHttpHeaders(String... extraHeaders) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("a", "alpha"); httpHeaders.add("b", "bravo"); From c0fac14fdf1f7628e83b24e93023357768009671 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 21:15:33 +0000 Subject: [PATCH 0216/1059] Break cycle caused by introduction of RestDocumentationHandler --- ...RestDocumentationGenerationException.java} | 8 +++--- .../RestDocumentationGenerator.java} | 28 +++++++++---------- .../restdocs/generate/package-info.java | 21 ++++++++++++++ ...a => RestDocumentationGeneratorTests.java} | 15 +++++----- .../mockmvc/MockMvcRestDocumentation.java | 10 +++---- .../RestDocumentationResultHandler.java | 6 ++-- .../RestAssuredRestDocumentation.java | 10 +++---- .../restassured/RestDocumentationFilter.java | 6 ++-- 8 files changed, 63 insertions(+), 41 deletions(-) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{RestDocumentationException.java => generate/RestDocumentationGenerationException.java} (80%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{RestDocumentationHandler.java => generate/RestDocumentationGenerator.java} (89%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{RestDocumentationHandlerTests.java => RestDocumentationGeneratorTests.java} (90%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java similarity index 80% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java index 5bdf27c13..e1e76d8c2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.generate; /** * An exception that can be thrown when a failure occurs during REST documentation @@ -22,14 +22,14 @@ * * @author Andy Wilkinson */ -public class RestDocumentationException extends RuntimeException { +public class RestDocumentationGenerationException extends RuntimeException { /** * Creates a new {@code RestDocumentationException} with the given {@code cause}. * * @param cause the cause */ - public RestDocumentationException(Throwable cause) { + public RestDocumentationGenerationException(Throwable cause) { super(cause); } @@ -40,7 +40,7 @@ public RestDocumentationException(Throwable cause) { * @param message the message * @param cause the cause */ - public RestDocumentationException(String message, Throwable cause) { + public RestDocumentationGenerationException(String message, Throwable cause) { super(message, cause); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java similarity index 89% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index f8be483b8..b60f5d9fd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.generate; import java.io.IOException; import java.util.ArrayList; @@ -36,14 +36,14 @@ import org.springframework.util.Assert; /** - * A {@code RestDocumentationHandler} is used to produce documentation snippets from the - * request and response of an operation performed on a service. + * A {@code RestDocumentationGenerator} is used to generate documentation snippets from + * the request and response of an operation performed on a service. * * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson */ -public final class RestDocumentationHandler { +public final class RestDocumentationGenerator { private final String identifier; @@ -58,7 +58,7 @@ public final class RestDocumentationHandler { private final ResponseConverter responseConverter; /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -69,7 +69,7 @@ public final class RestDocumentationHandler { * @param responseConverter the response converter * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, Snippet... snippets) { this(identifier, requestConverter, responseConverter, @@ -78,7 +78,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -92,7 +92,7 @@ public RestDocumentationHandler(String identifier, * @param requestPreprocessor the request preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { @@ -101,7 +101,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -115,7 +115,7 @@ public RestDocumentationHandler(String identifier, * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { @@ -125,7 +125,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -140,7 +140,7 @@ public RestDocumentationHandler(String identifier, * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, @@ -167,7 +167,7 @@ public RestDocumentationHandler(String identifier, * @param request the request * @param response the request * @param configuration the configuration - * @throws RestDocumentationException if a failure occurs during handling + * @throws RestDocumentationGenerationException if a failure occurs during handling */ public void handle(REQ request, RESP response, Map configuration) { OperationRequest operationRequest = this.requestPreprocessor @@ -184,7 +184,7 @@ public void handle(REQ request, RESP response, Map configuration } } catch (IOException ex) { - throw new RestDocumentationException(ex); + throw new RestDocumentationGenerationException(ex); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java new file mode 100644 index 000000000..6b6ba8636 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Classes that drive the generation of the documentaiton snippets. + */ +package org.springframework.restdocs.generate; + diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java similarity index 90% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index abfdafa55..d03aa6dae 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -26,6 +26,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; @@ -43,11 +44,11 @@ import static org.mockito.Mockito.verify; /** - * Tests for {@link RestDocumentationHandler}. + * Tests for {@link RestDocumentationGenerator}. * * @author Andy Wilkinson */ -public class RestDocumentationHandlerTests { +public class RestDocumentationGeneratorTests { @SuppressWarnings("unchecked") private final RequestConverter requestConverter = mock(RequestConverter.class); @@ -75,7 +76,7 @@ public void basicHandling() throws IOException { given(this.responseConverter.convert(this.response)).willReturn( this.operationResponse); HashMap configuration = new HashMap<>(); - new RestDocumentationHandler<>("id", this.requestConverter, + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); @@ -92,7 +93,7 @@ public void defaultSnippetsAreCalled() throws IOException { Snippet defaultSnippet2 = mock(Snippet.class); configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, Arrays.asList(defaultSnippet1, defaultSnippet2)); - new RestDocumentationHandler<>("id", this.requestConverter, + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); @@ -108,11 +109,11 @@ public void additionalSnippetsAreCalled() throws IOException { this.operationResponse); Snippet additionalSnippet1 = mock(Snippet.class); Snippet additionalSnippet2 = mock(Snippet.class); - RestDocumentationHandler handler = new RestDocumentationHandler<>( + RestDocumentationGenerator generator = new RestDocumentationGenerator<>( "id", this.requestConverter, this.responseConverter, this.snippet); - handler.addSnippets(additionalSnippet1, additionalSnippet2); + generator.addSnippets(additionalSnippet1, additionalSnippet2); HashMap configuration = new HashMap<>(); - handler.handle(this.request, this.response, configuration); + generator.handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); verifySnippetInvocation(additionalSnippet1, configuration); verifySnippetInvocation(additionalSnippet2, configuration); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 64825f7ea..cf1840cbe 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,7 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -66,7 +66,7 @@ public static MockMvcRestDocumentationConfigurer documentationConfiguration( */ public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } @@ -84,7 +84,7 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } @@ -103,7 +103,7 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } @@ -125,7 +125,7 @@ public static RestDocumentationResultHandler document(String identifier, public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 3194e826e..d536da132 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -20,7 +20,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -35,10 +35,10 @@ */ public class RestDocumentationResultHandler implements ResultHandler { - private final RestDocumentationHandler delegate; + private final RestDocumentationGenerator delegate; RestDocumentationResultHandler( - RestDocumentationHandler delegate) { + RestDocumentationGenerator delegate) { Assert.notNull(delegate, "delegate must be non-null"); this.delegate = delegate; } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 0e47cbd24..6f2c3f4ce 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -17,7 +17,7 @@ package org.springframework.restdocs.restassured; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -46,7 +46,7 @@ private RestAssuredRestDocumentation() { * @return a {@link RestDocumentationFilter} that will produce the documentation */ public static RestDocumentationFilter document(String identifier, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } @@ -62,7 +62,7 @@ public static RestDocumentationFilter document(String identifier, Snippet... sni */ public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } @@ -78,7 +78,7 @@ public static RestDocumentationFilter document(String identifier, */ public static RestDocumentationFilter document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } @@ -97,7 +97,7 @@ public static RestDocumentationFilter document(String identifier, public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index f588ed168..29a0b2571 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -20,7 +20,7 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.util.Assert; @@ -37,10 +37,10 @@ */ public final class RestDocumentationFilter implements Filter { - private final RestDocumentationHandler delegate; + private final RestDocumentationGenerator delegate; RestDocumentationFilter( - RestDocumentationHandler delegate) { + RestDocumentationGenerator delegate) { Assert.notNull(delegate, "delegate must be non-null"); this.delegate = delegate; } From cb9e10bf5af775c7f106138171fd8c274be10037 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 10:56:12 +0000 Subject: [PATCH 0217/1059] Break the package cycle caused by the introduction of SnippetFormat --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 8 +- .../com/example/mockmvc/CustomFormat.java | 4 +- .../com/example/restassured/CustomFormat.java | 4 +- .../config/RestDocumentationConfigurer.java | 4 +- .../restdocs/config/SnippetConfiguration.java | 10 +- .../restdocs/config/SnippetConfigurer.java | 20 ++-- .../restdocs/snippet/SnippetFormats.java | 74 --------------- .../snippet/StandardWriterResolver.java | 20 ++-- .../StandardTemplateResourceResolver.java | 19 ++-- .../TemplateFormat.java} | 18 +++- .../restdocs/templates/TemplateFormats.java | 93 +++++++++++++++++++ .../curl-request.snippet | 0 .../http-request.snippet | 0 .../http-response.snippet | 0 .../{adoc => asciidoctor}/links.snippet | 0 .../path-parameters.snippet | 0 .../request-fields.snippet | 0 .../request-headers.snippet | 0 .../request-parameters.snippet | 0 .../response-fields.snippet | 0 .../response-headers.snippet | 0 .../{md => markdown}/curl-request.snippet | 0 .../{md => markdown}/http-request.snippet | 0 .../{md => markdown}/http-response.snippet | 0 .../templates/{md => markdown}/links.snippet | 0 .../{md => markdown}/path-parameters.snippet | 0 .../{md => markdown}/request-fields.snippet | 0 .../{md => markdown}/request-headers.snippet | 0 .../request-parameters.snippet | 0 .../{md => markdown}/response-fields.snippet | 0 .../{md => markdown}/response-headers.snippet | 0 .../restdocs/AbstractSnippetTests.java | 30 +++--- .../RestDocumentationConfigurerTests.java | 14 +-- .../curl/CurlRequestSnippetTests.java | 6 +- .../RequestHeadersSnippetFailureTests.java | 4 +- .../headers/RequestHeadersSnippetTests.java | 6 +- .../ResponseHeadersSnippetFailureTests.java | 4 +- .../headers/ResponseHeadersSnippetTests.java | 6 +- .../http/HttpRequestSnippetTests.java | 6 +- .../http/HttpResponseSnippetTests.java | 6 +- .../hypermedia/LinksSnippetFailureTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 6 +- .../AsciidoctorRequestFieldsSnippetTests.java | 11 ++- .../RequestFieldsSnippetFailureTests.java | 4 +- .../payload/RequestFieldsSnippetTests.java | 6 +- .../ResponseFieldsSnippetFailureTests.java | 4 +- .../payload/ResponseFieldsSnippetTests.java | 6 +- .../PathParametersSnippetFailureTests.java | 4 +- .../request/PathParametersSnippetTests.java | 6 +- .../RequestParametersSnippetFailureTests.java | 4 +- .../RequestParametersSnippetTests.java | 6 +- .../snippet/StandardWriterResolverTests.java | 2 +- ...StandardTemplateResourceResolverTests.java | 6 +- .../restdocs/test/ExpectedSnippet.java | 12 +-- .../restdocs/test/OperationBuilder.java | 16 ++-- .../restdocs/test/SnippetMatchers.java | 54 +++++------ .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 ...quest-fields-with-list-description.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../{md => markdown}/links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 ...kMvcRestDocumentationIntegrationTests.java | 6 +- ...uredRestDocumentationIntegrationTests.java | 2 +- 94 files changed, 273 insertions(+), 244 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{snippet/SnippetFormat.java => templates/TemplateFormat.java} (65%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/curl-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/http-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/http-response.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/links.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/path-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/response-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/response-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/curl-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/http-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/http-response.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/links.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/path-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/response-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/response-headers.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-list-description.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-headers-with-title.snippet (100%) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 4c188cd45..c54f7a925 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index f068507e9..7941cb9ec 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -59,11 +59,11 @@ include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom- -[[configuration-snippet-format]] -=== Snippet format +[[configuration-snippet-template-format]] +=== Snippet template format -The default snippet format is Asciidoctor. Markdown is also supported out of the box. You -can change the default format using the `RestDocumentationConfigurer` API: +The default snippet template format is Asciidoctor. Markdown is also supported out of the +box. You can change the default format using the `RestDocumentationConfigurer` API: [source,java,indent=0,role="primary"] .MockMvc diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index ab121224a..3a32a9b53 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -22,7 +22,7 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -42,7 +42,7 @@ public void setUp() { // tag::custom-format[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation) - .snippets().withFormat(SnippetFormats.markdown())) + .snippets().withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] } diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 649becb25..c5424ec2a 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -38,7 +38,7 @@ public void setUp() { // tag::custom-format[] this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withFormat(SnippetFormats.markdown())) + .snippets().withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index ee721725b..f77c7aa45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -104,7 +104,7 @@ public void apply(Map configuration, .get(SnippetConfiguration.class.getName()); engineToUse = new MustacheTemplateEngine( new StandardTemplateResourceResolver( - snippetConfiguration.getFormat())); + snippetConfiguration.getTemplateFormat())); } configuration.put(TemplateEngine.class.getName(), engineToUse); } @@ -129,7 +129,7 @@ public void apply(Map configuration, resolverToUse = new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context), snippetConfiguration.getEncoding(), - snippetConfiguration.getFormat()); + snippetConfiguration.getTemplateFormat()); } configuration.put(WriterResolver.class.getName(), resolverToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java index ecfd94e20..a1ced2167 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.config; -import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.templates.TemplateFormat; /** * An encapsulation of the configuration for documentation snippets. @@ -27,18 +27,18 @@ class SnippetConfiguration { private final String encoding; - private final SnippetFormat format; + private final TemplateFormat format; - SnippetConfiguration(String encoding, SnippetFormat format) { + SnippetConfiguration(String encoding, TemplateFormat templateFormat) { this.encoding = encoding; - this.format = format; + this.format = templateFormat; } String getEncoding() { return this.encoding; } - SnippetFormat getFormat() { + TemplateFormat getTemplateFormat() { return this.format; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index a028a7c6f..b82ee82cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -24,8 +24,8 @@ import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; /** * A configurer that can be used to configure the generated documentation snippets. @@ -55,14 +55,14 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

      configuration, RestDocumentationContext context) { configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( - this.snippetEncoding, this.snippetFormat)); + this.snippetEncoding, this.templateFormat)); configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); } @@ -106,14 +106,14 @@ public T withDefaults(Snippet... defaultSnippets) { } /** - * Configures the format of the documentation snippets. + * Configures the format of the documentation snippet templates. * - * @param format the snippet format + * @param format the snippet template format * @return {@code this} */ @SuppressWarnings("unchecked") - public T withFormat(SnippetFormat format) { - this.snippetFormat = format; + public T withTemplateFormat(TemplateFormat format) { + this.templateFormat = format; return (T) this; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java deleted file mode 100644 index 8c24861a4..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2014-2016 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.snippet; - -/** - * An enumeration of the built-in snippet formats. - * - * @author Andy Wilkinson - */ -public abstract class SnippetFormats { - - private static final SnippetFormat ASCIIDOCTOR = new AsciidoctorSnippetFormat(); - - private static final SnippetFormat MARKDOWN = new MarkdownSnippetFormat(); - - private SnippetFormats() { - - } - - /** - * Returns the Asciidoctor snippet format. - * - * @return the snippet format - */ - public static SnippetFormat asciidoctor() { - return ASCIIDOCTOR; - } - - /** - * Returns the Markdown snippet format. - * - * @return the snippet format - */ - public static SnippetFormat markdown() { - return MARKDOWN; - } - - private static final class AsciidoctorSnippetFormat implements SnippetFormat { - - private static final String FILE_EXTENSION = "adoc"; - - @Override - public String getFileExtension() { - return FILE_EXTENSION; - } - - } - - private static final class MarkdownSnippetFormat implements SnippetFormat { - - private static final String FILE_EXTENSION = "md"; - - @Override - public String getFileExtension() { - return FILE_EXTENSION; - } - - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1538da24b..cf2e0ed34 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -23,6 +23,8 @@ import java.io.Writer; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -40,7 +42,7 @@ public final class StandardWriterResolver implements WriterResolver { private String encoding = "UTF-8"; - private SnippetFormat snippetFormat; + private TemplateFormat templateFormat; /** * Creates a new {@code StandardWriterResolver} that will use the given @@ -50,29 +52,29 @@ public final class StandardWriterResolver implements WriterResolver { * * @param placeholderResolver the placeholder resolver * @deprecated since 1.1.0 in favor of - * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, SnippetFormat)} + * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, TemplateFormat)} */ @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { - this(placeholderResolver, "UTF-8", SnippetFormats.asciidoctor()); + this(placeholderResolver, "UTF-8", TemplateFormats.asciidoctor()); } /** * Creates a new {@code StandardWriterResolver} that will use the given * {@code placeholderResolver} to resolve any placeholders in the * {@code operationName}. Writers will use the given {@code encoding} and, when - * writing to a file, will use a filename appropriate for content in the given - * {@code snippetFormat}. + * writing to a file, will use a filename appropriate for content generated from + * templates in the given {@code templateFormat}. * * @param placeholderResolver the placeholder resolver * @param encoding the encoding - * @param snippetFormat the snippet format + * @param templateFormat the snippet format */ public StandardWriterResolver(PlaceholderResolver placeholderResolver, - String encoding, SnippetFormat snippetFormat) { + String encoding, TemplateFormat templateFormat) { this.placeholderResolver = placeholderResolver; this.encoding = encoding; - this.snippetFormat = snippetFormat; + this.templateFormat = templateFormat; } @Override @@ -80,7 +82,7 @@ public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( operationName, this.placeholderResolver), snippetName + "." - + this.snippetFormat.getFileExtension(), context); + + this.templateFormat.getFileExtension(), context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index b8b2c62e8..22912eb36 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -18,8 +18,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; /** * Standard implementation of {@link TemplateResourceResolver}. @@ -33,28 +31,28 @@ */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; /** * Creates a new {@code StandardTemplateResourceResolver} that will produce default * template resources formatted with Asciidoctor. * * @deprecated since 1.1.0 in favour of - * {@link #StandardTemplateResourceResolver(SnippetFormat)} + * {@link #StandardTemplateResourceResolver(TemplateFormat)} */ @Deprecated public StandardTemplateResourceResolver() { - this(SnippetFormats.asciidoctor()); + this(TemplateFormats.asciidoctor()); } /** * Creates a new {@code StandardTemplateResourceResolver} that will produce default - * template resources formatted with the given {@code snippetFormat}. + * template resources formatted with the given {@code templateFormat}. * - * @param snippetFormat the format for the default snippet templates + * @param templateFormat the format for the default snippet templates */ - public StandardTemplateResourceResolver(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + public StandardTemplateResourceResolver(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } @Override @@ -64,8 +62,7 @@ public Resource resolveTemplateResource(String name) { if (!classPathResource.exists()) { classPathResource = new ClassPathResource( "org/springframework/restdocs/templates/" - + this.snippetFormat.getFileExtension() + "/" + name - + ".snippet"); + + this.templateFormat.getId() + "/" + name + ".snippet"); if (!classPathResource.exists()) { throw new IllegalStateException("Template named '" + name + "' could not be resolved"); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java similarity index 65% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java index 58e31f04e..972103607 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java @@ -14,18 +14,26 @@ * limitations under the License. */ -package org.springframework.restdocs.snippet; +package org.springframework.restdocs.templates; /** - * A {@link SnippetFormat} provides information about a particular snippet format, such as - * Asciidoctor or Markdown. + * A {@link TemplateFormat} provides information about a particular template format, such + * as Asciidoctor or Markdown. * * @author Andy Wilkinson */ -public interface SnippetFormat { +public interface TemplateFormat { /** - * Returns the snippet format's file extension. + * Returns the id of this template format. + * + * @return the id + */ + String getId(); + + /** + * Returns the file extension to use for files generated from templates in this + * format. * * @return the file extension */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java new file mode 100644 index 000000000..537f01ed5 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014-2016 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.templates; + +/** + * An enumeration of the built-in formats for which templates are provuded. + * + * @author Andy Wilkinson + */ +public abstract class TemplateFormats { + + private static final TemplateFormat ASCIIDOCTOR = new AsciidoctorTemplateFormat(); + + private static final TemplateFormat MARKDOWN = new MarkdownTemplateFormat(); + + private TemplateFormats() { + + } + + /** + * Returns the Asciidoctor template format with the ID {@code asciidoctor} and the + * file extension {@code adoc}. + * + * @return the template format + */ + public static TemplateFormat asciidoctor() { + return ASCIIDOCTOR; + } + + /** + * Returns the Markdown template format with the ID {@code markdown} and the file + * extension {@code md}. + * + * @return the template format + */ + public static TemplateFormat markdown() { + return MARKDOWN; + } + + private abstract static class AbstractTemplateFormat implements TemplateFormat { + + private final String name; + + private final String fileExtension; + + private AbstractTemplateFormat(String name, String fileExtension) { + this.name = name; + this.fileExtension = fileExtension; + } + + @Override + public String getId() { + return this.name; + } + + @Override + public String getFileExtension() { + return this.fileExtension; + } + + } + + private static final class AsciidoctorTemplateFormat extends AbstractTemplateFormat { + + private AsciidoctorTemplateFormat() { + super("asciidoctor", "adoc"); + } + + } + + private static final class MarkdownTemplateFormat extends AbstractTemplateFormat { + + private MarkdownTemplateFormat() { + super("markdown", "md"); + } + + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 83017f29d..edf0f1442 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -25,7 +25,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import org.springframework.restdocs.test.SnippetMatchers; @@ -35,8 +35,8 @@ import org.springframework.restdocs.test.SnippetMatchers.TableMatcher; import org.springframework.web.bind.annotation.RequestMethod; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; -import static org.springframework.restdocs.snippet.SnippetFormats.markdown; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; /** * Abstract base class for testing snippet generation. @@ -46,7 +46,7 @@ @RunWith(Parameterized.class) public abstract class AbstractSnippetTests { - protected final SnippetFormat snippetFormat; + protected final TemplateFormat templateFormat; @Rule public ExpectedSnippet snippet; @@ -57,40 +57,40 @@ public static List parameters() { "Markdown", markdown() }); } - public AbstractSnippetTests(String name, SnippetFormat snippetFormat) { - this.snippet = new ExpectedSnippet(snippetFormat); - this.snippetFormat = snippetFormat; + public AbstractSnippetTests(String name, TemplateFormat templateFormat) { + this.snippet = new ExpectedSnippet(templateFormat); + this.templateFormat = templateFormat; } public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.snippetFormat, language); + return SnippetMatchers.codeBlock(this.templateFormat, language); } public TableMatcher tableWithHeader(String... headers) { - return SnippetMatchers.tableWithHeader(this.snippetFormat, headers); + return SnippetMatchers.tableWithHeader(this.templateFormat, headers); } public TableMatcher tableWithTitleAndHeader(String title, String... headers) { - return SnippetMatchers - .tableWithTitleAndHeader(this.snippetFormat, title, headers); + return SnippetMatchers.tableWithTitleAndHeader(this.templateFormat, title, + headers); } public HttpRequestMatcher httpRequest(RequestMethod method, String uri) { - return SnippetMatchers.httpRequest(this.snippetFormat, method, uri); + return SnippetMatchers.httpRequest(this.templateFormat, method, uri); } public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { - return SnippetMatchers.httpResponse(this.snippetFormat, responseStatus); + return SnippetMatchers.httpResponse(this.templateFormat, responseStatus); } public OperationBuilder operationBuilder(String name) { return new OperationBuilder(name, this.snippet.getOutputDirectory(), - this.snippetFormat); + this.templateFormat); } protected FileSystemResource snippetResource(String name) { return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + this.snippetFormat.getFileExtension() + "/" + name + ".snippet"); + + this.templateFormat.getId() + "/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 2dad00fe9..9819d8c54 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -28,10 +28,10 @@ import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import static org.hamcrest.CoreMatchers.equalTo; @@ -83,8 +83,8 @@ public void defaultConfiguration() { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); assertThat(snippetConfiguration.getEncoding(), is(equalTo("UTF-8"))); - assertThat(snippetConfiguration.getFormat(), - is(equalTo(SnippetFormats.asciidoctor()))); + assertThat(snippetConfiguration.getTemplateFormat(), + is(equalTo(TemplateFormats.asciidoctor()))); } @Test @@ -139,10 +139,10 @@ public void customSnippetEncoding() { } @Test - public void customSnippetFormat() { + public void customTemplateFormat() { RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.snippets().withFormat(SnippetFormats.markdown()) + this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()) .apply(configuration, context); assertThat( configuration, @@ -150,8 +150,8 @@ public void customSnippetFormat() { instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); - assertThat(snippetConfiguration.getFormat(), - is(equalTo(SnippetFormats.markdown()))); + assertThat(snippetConfiguration.getTemplateFormat(), + is(equalTo(TemplateFormats.markdown()))); } private static final class TestRestDocumentationConfigurer diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f93b04674..645866c78 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -24,8 +24,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.util.Base64Utils; @@ -48,8 +48,8 @@ @RunWith(Parameterized.class) public class CurlRequestSnippetTests extends AbstractSnippetTests { - public CurlRequestSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index ca011d2b3..9a5a2a7e7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class RequestHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index ed076d263..d9732a90f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -42,8 +42,8 @@ */ public class RequestHeadersSnippetTests extends AbstractSnippetTests { - public RequestHeadersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index 382e91251..fae546279 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class ResponseHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index f58805e01..33a53cb83 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -42,8 +42,8 @@ */ public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - public ResponseHeadersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 41e90bfb5..44bd57869 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -22,8 +22,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.web.bind.annotation.RequestMethod; @@ -44,8 +44,8 @@ public class HttpRequestSnippetTests extends AbstractSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - public HttpRequestSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 96bc7be41..8d63d175a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class HttpResponseSnippetTests extends AbstractSnippetTests { - public HttpResponseSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 335127cfb..ce16c6351 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -39,7 +39,7 @@ public class LinksSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index c3bd55a74..4c22d8b28 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -39,8 +39,8 @@ */ public class LinksSnippetTests extends AbstractSnippetTests { - public LinksSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public LinksSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index a56e8ed5b..000fabd60 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -22,8 +22,8 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; @@ -32,7 +32,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** @@ -43,7 +43,7 @@ public class AsciidoctorRequestFieldsSnippetTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Test public void requestFieldsWithListDescription() throws IOException { @@ -67,8 +67,9 @@ public void requestFieldsWithListDescription() throws IOException { } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/adoc/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/asciidoctor/" + name + + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 442616b61..f4a762ce5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -44,7 +44,7 @@ public class RequestFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 7f9a665a6..ff0b5c95e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class RequestFieldsSnippetTests extends AbstractSnippetTests { - public RequestFieldsSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index d664b8074..6591d714f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -44,7 +44,7 @@ public class ResponseFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index f4e00111d..623d51491 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - public ResponseFieldsSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 7f9a07b84..06310faf7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class PathParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 9d148769e..7f989a82c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -40,8 +40,8 @@ */ public class PathParametersSnippetTests extends AbstractSnippetTests { - public PathParametersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index b795dd572..849b0d5d7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class RequestParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 2facc40c6..944db79e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -40,8 +40,8 @@ */ public class RequestParametersSnippetTests extends AbstractSnippetTests { - public RequestParametersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 31bdb75dc..3f2406a53 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -26,7 +26,7 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for {@link StandardWriterResolver}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index 00a3c8d4a..8d8ef578a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -29,7 +29,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for {@link TemplateResourceResolver}. @@ -68,8 +68,8 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/adoc/test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/asciidoctor/test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 6a571f350..1bc8b49c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,8 +23,8 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; import static org.hamcrest.CoreMatchers.is; @@ -39,7 +39,7 @@ */ public class ExpectedSnippet implements TestRule { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private final SnippetMatcher snippet; @@ -49,9 +49,9 @@ public class ExpectedSnippet implements TestRule { private File outputDirectory; - public ExpectedSnippet(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; - this.snippet = SnippetMatchers.snippet(snippetFormat); + public ExpectedSnippet(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + this.snippet = SnippetMatchers.snippet(templateFormat); } @Override @@ -65,7 +65,7 @@ private void verifySnippet() throws IOException { if (this.outputDirectory != null && this.expectedName != null) { File snippetDir = new File(this.outputDirectory, this.expectedName); File snippetFile = new File(snippetDir, this.expectedType + "." - + this.snippetFormat.getFileExtension()); + + this.templateFormat.getFileExtension()); assertThat(snippetFile, is(this.snippet)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e2a54b998..b9e050d93 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -37,12 +37,12 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -60,18 +60,18 @@ public class OperationBuilder { private final File outputDirectory; - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private OperationRequestBuilder requestBuilder; public OperationBuilder(String name, File outputDirectory) { - this(name, outputDirectory, SnippetFormats.asciidoctor()); + this(name, outputDirectory, TemplateFormats.asciidoctor()); } - public OperationBuilder(String name, File outputDirectory, SnippetFormat snippetFormat) { + public OperationBuilder(String name, File outputDirectory, TemplateFormat templateFormat) { this.name = name; this.outputDirectory = outputDirectory; - this.snippetFormat = snippetFormat; + this.templateFormat = templateFormat; } public OperationRequestBuilder request(String uri) { @@ -92,14 +92,14 @@ public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine(new StandardTemplateResourceResolver( - this.snippetFormat))); + this.templateFormat))); } RestDocumentationContext context = new RestDocumentationContext(null, null, this.outputDirectory); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context), "UTF-8", - this.snippetFormat)); + this.templateFormat)); return new StandardOperation(this.name, (this.requestBuilder == null ? new OperationRequestBuilder( "http://localhost/").buildRequest() : this.requestBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index c076d0ca7..c08b937b9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -30,8 +30,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -47,18 +47,19 @@ private SnippetMatchers() { } - public static SnippetMatcher snippet(SnippetFormat snippetFormat) { - return new SnippetMatcher(snippetFormat); + public static SnippetMatcher snippet(TemplateFormat templateFormat) { + return new SnippetMatcher(templateFormat); } - public static TableMatcher tableWithHeader(SnippetFormat format, String... headers) { + public static TableMatcher tableWithHeader(TemplateFormat format, + String... headers) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorTableMatcher(null, headers); } return new MarkdownTableMatcher(null, headers); } - public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, + public static TableMatcher tableWithTitleAndHeader(TemplateFormat format, String title, String... headers) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorTableMatcher(title, headers); @@ -66,7 +67,7 @@ public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, return new MarkdownTableMatcher(title, headers); } - public static HttpRequestMatcher httpRequest(SnippetFormat format, + public static HttpRequestMatcher httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { if ("adoc".equals(format.getFileExtension())) { return new HttpRequestMatcher(requestMethod, uri, @@ -76,7 +77,8 @@ public static HttpRequestMatcher httpRequest(SnippetFormat format, "http"), 2); } - public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus status) { + public static HttpResponseMatcher httpResponse(TemplateFormat format, + HttpStatus status) { if ("adoc".equals(format.getFileExtension())) { return new HttpResponseMatcher(status, new AsciidoctorCodeBlockMatcher<>( "http"), 3); @@ -85,7 +87,7 @@ public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus } @SuppressWarnings({ "rawtypes" }) - public static CodeBlockMatcher codeBlock(SnippetFormat format, String language) { + public static CodeBlockMatcher codeBlock(TemplateFormat format, String language) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorCodeBlockMatcher(language); } @@ -95,12 +97,12 @@ public static CodeBlockMatcher codeBlock(SnippetFormat format, String languag private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private List lines = new ArrayList<>(); - protected AbstractSnippetContentMatcher(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + protected AbstractSnippetContentMatcher(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } protected void addLine(String line) { @@ -121,7 +123,7 @@ public boolean matches(Object item) { @Override public void describeTo(Description description) { - description.appendText(this.snippetFormat.getFileExtension() + " snippet"); + description.appendText(this.templateFormat.getFileExtension() + " snippet"); description.appendText(getLinesAsString()); } @@ -157,8 +159,8 @@ private String getLinesAsString() { public static class CodeBlockMatcher> extends AbstractSnippetContentMatcher { - protected CodeBlockMatcher(SnippetFormat snippetFormat) { - super(snippetFormat); + protected CodeBlockMatcher(TemplateFormat templateFormat) { + super(templateFormat); } @SuppressWarnings("unchecked") @@ -178,7 +180,7 @@ public static class AsciidoctorCodeBlockMatcher { protected AsciidoctorCodeBlockMatcher(String language) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); this.addLine("[source," + language + "]"); this.addLine("----"); this.addLine("----"); @@ -195,7 +197,7 @@ public static class MarkdownCodeBlockMatcher { protected MarkdownCodeBlockMatcher(String language) { - super(SnippetFormats.markdown()); + super(TemplateFormats.markdown()); this.addLine("```" + language); this.addLine("```"); } @@ -286,8 +288,8 @@ private HttpRequestMatcher(RequestMethod requestMethod, String uri, public static abstract class TableMatcher> extends AbstractSnippetContentMatcher { - protected TableMatcher(SnippetFormat snippetFormat) { - super(snippetFormat); + protected TableMatcher(TemplateFormat templateFormat) { + super(templateFormat); } public abstract T row(String... entries); @@ -303,7 +305,7 @@ public static final class AsciidoctorTableMatcher extends TableMatcher { private AsciidoctorTableMatcher(String title, String... columns) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine("." + title); } @@ -340,7 +342,7 @@ public static final class MarkdownTableMatcher extends TableMatcher { private MarkdownTableMatcher(String title, String... columns) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine(title); } @@ -379,12 +381,12 @@ public MarkdownTableMatcher configuration(String configuration) { */ public static final class SnippetMatcher extends BaseMatcher { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private Matcher expectedContents; - private SnippetMatcher(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + private SnippetMatcher(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } @Override @@ -435,8 +437,8 @@ public void describeTo(Description description) { this.expectedContents.describeTo(description); } else { - description - .appendText(this.snippetFormat.getFileExtension() + " snippet"); + description.appendText(this.templateFormat.getFileExtension() + + " snippet"); } } diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-list-description.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-list-description.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-title.snippet diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 0c48c4801..ac83d9719 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -75,8 +75,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; -import static org.springframework.restdocs.snippet.SnippetFormats.markdown; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -130,7 +130,7 @@ public void markdownSnippetGeneration() throws Exception { MockMvc mockMvc = MockMvcBuilders .webAppContextSetup(this.context) .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) - .snippets().withEncoding("UTF-8").withFormat(markdown())).build(); + .snippets().withEncoding("UTF-8").withTemplateFormat(markdown())).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic-markdown")); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index eeea60b48..ff3084d84 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -70,7 +70,7 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; From 811e7adf70c97682cc17d70a2bb31c91294128f3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 11:49:04 +0000 Subject: [PATCH 0218/1059] Add missing package-info.java for config package --- .../restdocs/config/package-info.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java new file mode 100644 index 000000000..f338eae83 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Classes for configuring Spring REST Docs. + */ +package org.springframework.restdocs.config; + From 83b72f7962829abdc77d8eb38bae8e0ac86f22a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 20:57:46 +0000 Subject: [PATCH 0219/1059] Introduce constants for common attribute names Closes gh-193 --- .../restdocs/config/SnippetConfigurer.java | 9 ++---- .../generate/RestDocumentationGenerator.java | 29 ++++++++++++------- .../request/PathParametersSnippet.java | 3 +- .../RestDocumentationGeneratorTests.java | 3 +- .../RestDocumentationConfigurerTests.java | 11 ++++--- .../PathParametersSnippetFailureTests.java | 10 +++++-- .../request/PathParametersSnippetTests.java | 24 ++++++++++----- .../MockMvcRestDocumentationConfigurer.java | 10 ++++--- .../RestDocumentationRequestBuilders.java | 23 ++++++++------- .../RestDocumentationResultHandler.java | 4 ++- ...RestDocumentationRequestBuildersTests.java | 4 ++- ...estAssuredRestDocumentationConfigurer.java | 2 +- .../restassured/RestDocumentationFilter.java | 6 ++-- ...suredRestDocumentationConfigurerTests.java | 8 +++-- 14 files changed, 89 insertions(+), 57 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index b82ee82cf..0827c519e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -22,6 +22,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; @@ -36,11 +37,6 @@ */ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

      { - /** - * The name of the attribute that is used to hold the default snippets. - */ - public static final String ATTRIBUTE_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; - private List defaultSnippets = Arrays.asList( CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); @@ -77,7 +73,8 @@ protected SnippetConfigurer(P parent) { public void apply(Map configuration, RestDocumentationContext context) { configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( this.snippetEncoding, this.templateFormat)); - configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, + this.defaultSnippets); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index b60f5d9fd..2a46d7e13 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -45,6 +44,16 @@ */ public final class RestDocumentationGenerator { + /** + * Name of the operation attribute used to hold the request's URL template. + */ + public static final String ATTRIBUTE_NAME_URL_TEMPLATE = "org.springframework.restdocs.urlTemplate"; + + /** + * Name of the operation attribute used to hold the {@link List} of default snippets. + */ + public static final String ATTRIBUTE_NAME_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; + private final String identifier; private final OperationRequestPreprocessor requestPreprocessor; @@ -58,8 +67,8 @@ public final class RestDocumentationGenerator { private final ResponseConverter responseConverter; /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given documentation {@code snippets} will be produced. @@ -78,8 +87,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code requestPreprocessor} is applied to the @@ -101,8 +110,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code responsePreprocessor} is applied to the @@ -125,8 +134,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code requestPreprocessor} and @@ -202,7 +211,7 @@ public void addSnippets(Snippet... snippets) { private List getSnippets(Map configuration) { List combinedSnippets = new ArrayList<>(this.snippets); List defaultSnippets = (List) configuration - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS); + .get(ATTRIBUTE_NAME_DEFAULT_SNIPPETS); if (defaultSnippets != null) { combinedSnippets.addAll(defaultSnippets); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 0670be1da..b09b59fab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -23,6 +23,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; @@ -91,7 +92,7 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes().get( - "org.springframework.restdocs.urlTemplate"); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); Assert.notNull( urlTemplate, "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index d03aa6dae..a202eb350 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -25,7 +25,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -91,7 +90,7 @@ public void defaultSnippetsAreCalled() throws IOException { HashMap configuration = new HashMap<>(); Snippet defaultSnippet1 = mock(Snippet.class); Snippet defaultSnippet2 = mock(Snippet.class); - configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, Arrays.asList(defaultSnippet1, defaultSnippet2)); new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 9819d8c54..a18eebc77 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; import org.springframework.restdocs.snippet.Snippet; @@ -67,10 +68,11 @@ public void defaultConfiguration() { instanceOf(StandardWriterResolver.class))); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); List defaultSnippets = (List) configuration - .get("org.springframework.restdocs.defaultSnippets"); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat( defaultSnippets, contains(instanceOf(CurlRequestSnippet.class), @@ -115,11 +117,12 @@ public void customDefaultSnippets() { .apply(configuration, context); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); @SuppressWarnings("unchecked") List defaultSnippets = (List) configuration - .get("org.springframework.restdocs.defaultSnippets"); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 06310faf7..c389c2803 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; @@ -53,7 +54,8 @@ public void undocumentedPathParameter() throws IOException { new PathParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-path-parameter", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/") + .build()); } @Test @@ -65,7 +67,8 @@ public void missingPathParameter() throws IOException { Arrays.asList(parameterWithName("a").description("one"))) .document(new OperationBuilder("missing-path-parameter", this.snippet .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/") + .build()); } @Test @@ -79,7 +82,8 @@ public void undocumentedAndMissingPathParameters() throws IOException { .document(new OperationBuilder( "undocumented-and-missing-path-parameters", this.snippet .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{b}") + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 7f989a82c..bb17db106 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -51,9 +52,10 @@ public void pathParameters() throws IOException { "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(operationBuilder("path-parameters") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .build()); + .description("two"))) + .document(operationBuilder("path-parameters").attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}").build()); } @Test @@ -64,7 +66,8 @@ public void ignoredPathParameter() throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))).document(operationBuilder( "ignored-path-parameter").attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); } @Test @@ -77,7 +80,8 @@ public void pathParametersWithQueryString() throws IOException { parameterWithName("a").description("one"), parameterWithName("b") .description("two"))).document(operationBuilder( "path-parameters-with-query-string").attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo=bar").build()); } @Test @@ -93,7 +97,8 @@ public void pathParametersWithCustomAttributes() throws IOException { .description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document(operationBuilder( "path-parameters-with-custom-attributes") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); @@ -112,8 +117,11 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document(operationBuilder("path-parameters-with-custom-descriptor-attributes") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .document(operationBuilder( + "path-parameters-with-custom-descriptor-attributes") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index 7921cab8c..e20ecd01b 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -23,6 +23,7 @@ import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -90,11 +91,12 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) RestDocumentationContext context = this.restDocumentation.beforeOperation(); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); - String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; - configuration.put(urlTemplateAttribute, - request.getAttribute(urlTemplateAttribute)); + configuration + .put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE)); configuration.put(RestDocumentationContext.class.getName(), context); - request.setAttribute("org.springframework.restdocs.configuration", + request.setAttribute( + RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION, configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 2c9dc4529..ffe7220da 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -19,6 +19,7 @@ import java.net.URI; import org.springframework.http.HttpMethod; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.request.RequestDocumentation; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; @@ -38,8 +39,6 @@ */ public abstract class RestDocumentationRequestBuilders { - private static final String ATTRIBUTE_NAME_URL_TEMPLATE = "org.springframework.restdocs.urlTemplate"; - private RestDocumentationRequestBuilders() { } @@ -55,7 +54,7 @@ private RestDocumentationRequestBuilders() { public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -79,7 +78,7 @@ public static MockHttpServletRequestBuilder get(URI uri) { public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -103,7 +102,7 @@ public static MockHttpServletRequestBuilder post(URI uri) { public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -127,7 +126,7 @@ public static MockHttpServletRequestBuilder put(URI uri) { public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -151,7 +150,7 @@ public static MockHttpServletRequestBuilder patch(URI uri) { public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -175,7 +174,7 @@ public static MockHttpServletRequestBuilder delete(URI uri) { public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -199,7 +198,7 @@ public static MockHttpServletRequestBuilder options(URI uri) { public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -224,7 +223,8 @@ public static MockHttpServletRequestBuilder head(URI uri) { public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.request(httpMethod, urlTemplate, urlVariables) - .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + urlTemplate); } /** @@ -250,7 +250,8 @@ public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTempla Object... urlVariables) { return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders .fileUpload(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + urlTemplate); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index d536da132..ac43bdd68 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -35,6 +35,8 @@ */ public class RestDocumentationResultHandler implements ResultHandler { + static final String ATTRIBUTE_NAME_CONFIGURATION = "org.springframework.restdocs.configuration"; + private final RestDocumentationGenerator delegate; RestDocumentationResultHandler( @@ -47,7 +49,7 @@ public class RestDocumentationResultHandler implements ResultHandler { public void handle(MvcResult result) throws Exception { @SuppressWarnings("unchecked") Map configuration = (Map) result.getRequest() - .getAttribute("org.springframework.restdocs.configuration"); + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION); this.delegate.handle(result.getRequest(), result.getResponse(), configuration); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index a5bd3159f..069687896 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -24,6 +24,7 @@ import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -143,7 +144,8 @@ private void assertTemplate(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { MockHttpServletRequest request = builder.buildRequest(this.servletContext); assertThat( - (String) request.getAttribute("org.springframework.restdocs.urlTemplate"), + (String) request + .getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE), is(equalTo("{template}"))); assertThat(request.getRequestURI(), is(equalTo("t"))); assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 25afe0ec2..4702a752d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -59,7 +59,7 @@ public Response filter(FilterableRequestSpecification requestSpec, RestDocumentationContext context = this.restDocumentation.beforeOperation(); filterContext.setValue(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); - filterContext.setValue("org.springframework.restdocs.configuration", + filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, configuration); apply(configuration, context); return filterContext.next(requestSpec, responseSpec); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 29a0b2571..fdb3b0681 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -37,6 +37,8 @@ */ public final class RestDocumentationFilter implements Filter { + static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; + private final RestDocumentationGenerator delegate; RestDocumentationFilter( @@ -51,11 +53,11 @@ public Response filter(FilterableRequestSpecification requestSpec, Response response = context.next(requestSpec, responseSpec); Map configuration = new HashMap<>( - context.>getValue("org.springframework.restdocs.configuration")); + context.>getValue(CONTEXT_KEY_CONFIGURATION)); configuration.put(RestDocumentationContext.class.getName(), context .getValue(RestDocumentationContext.class .getName())); - configuration.put("org.springframework.restdocs.urlTemplate", + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); this.delegate.handle(requestSpec, response, configuration); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 1f7bbbd44..6b4e02c68 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; @@ -62,7 +63,7 @@ public class RestAssuredRestDocumentationConfigurerTests { public void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); verify(this.filterContext).setValue( - eq("org.springframework.restdocs.configuration"), any(Map.class)); + eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), any(Map.class)); } @Test @@ -71,7 +72,7 @@ public void configurationIsAddedToTheContext() { @SuppressWarnings("rawtypes") ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); verify(this.filterContext).setValue( - eq("org.springframework.restdocs.configuration"), + eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), configurationCaptor.capture()); @SuppressWarnings("unchecked") Map configuration = configurationCaptor.getValue(); @@ -85,7 +86,8 @@ public void configurationIsAddedToTheContext() { instanceOf(WriterResolver.class))); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); } } From 6578f4273069357d21d6138f2b1b97aa207763ea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 13:41:24 +0000 Subject: [PATCH 0220/1059] Allow custom templates to be provided for a specific template format Previously, custom snippet templates were loaded from org/springframework/restdocs/templates and the default templates were loaded from org/springframework/restdocs/templates/{$formatId}. Without relying on the ordering of the classpath, this made it impossible to provide a custom template for a specific format. This commit updates the locations that are checked for snippet templates. The following locations are now checked in order: 1. org/springframework/restdocs/templates/${formatId}/${name}.snippet 2. org/springframework/restdocs/templates/${name}.snippet 3. org/springframework/restdocs/templates/${formatId}/default-${name}.snippet The second location is provided largely for backwards compatibility with 1.0. Users are expected to use the first location to provide any custom templates, with Spring REST Docs provided templates for all of the built-in snippets in the third location. Closes gh-196 Closes gh-197 --- .../docs/asciidoc/documenting-your-api.adoc | 10 +++- .../StandardTemplateResourceResolver.java | 57 ++++++++++++++----- ...t.snippet => default-curl-request.snippet} | 0 ...t.snippet => default-http-request.snippet} | 0 ....snippet => default-http-response.snippet} | 0 .../{links.snippet => default-links.snippet} | 0 ...nippet => default-path-parameters.snippet} | 0 ...snippet => default-request-fields.snippet} | 0 ...nippet => default-request-headers.snippet} | 0 ...pet => default-request-parameters.snippet} | 0 ...nippet => default-response-fields.snippet} | 0 ...ippet => default-response-headers.snippet} | 0 ...t.snippet => default-curl-request.snippet} | 0 ...t.snippet => default-http-request.snippet} | 0 ....snippet => default-http-response.snippet} | 0 .../{links.snippet => default-links.snippet} | 0 ...nippet => default-path-parameters.snippet} | 0 ...snippet => default-request-fields.snippet} | 0 ...nippet => default-request-headers.snippet} | 0 ...pet => default-request-parameters.snippet} | 0 ...nippet => default-response-fields.snippet} | 0 ...ippet => default-response-headers.snippet} | 0 ...StandardTemplateResourceResolverTests.java | 52 ++++++++++++++--- .../{test.snippet => test-custom.snippet} | 0 .../restdocs/templates/test-default.snippet | 0 .../test-format-specific-custom.snippet | 0 26 files changed, 93 insertions(+), 26 deletions(-) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{curl-request.snippet => default-curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{http-request.snippet => default-http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{http-response.snippet => default-http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{links.snippet => default-links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{path-parameters.snippet => default-path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-fields.snippet => default-request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-headers.snippet => default-request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-parameters.snippet => default-request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{response-fields.snippet => default-response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{response-headers.snippet => default-response-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{curl-request.snippet => default-curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{http-request.snippet => default-http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{http-response.snippet => default-http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{links.snippet => default-links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{path-parameters.snippet => default-path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-fields.snippet => default-request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-headers.snippet => default-request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-parameters.snippet => default-request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{response-fields.snippet => default-response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{response-headers.snippet => default-response-headers.snippet} (100%) rename spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/{test.snippet => test-custom.snippet} (100%) create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c68b9d43a..fb9977c85 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -627,10 +627,14 @@ generated snippets. templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. -Templates are loaded from the classpath in the `org.springframework.restdocs.templates` -package and each template is named after the snippet that it will produce. For example, to +Templates are loaded from the classpath from an `org.springframework.restdocs.templates` +subpackage. The name of the subpackage is determined by the ID of the template format +that is in use. The default template format, Asciidoctor, has the ID `asciidoctor` so +snippets are loaded from `org.springframework.restdocs.templates.asciidoctor`. Each +template is named after the snippet that it will produce. For example, to override the template for the `curl-request.adoc` snippet, create a template named -`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. +`curl-request.snippet` in +`src/test/resources/org/springframework/restdocs/templates/asciidoctor`. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 22912eb36..7e0b38ac8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -22,12 +22,20 @@ /** * Standard implementation of {@link TemplateResourceResolver}. *

      - * Templates are resolved by looking for a resource on the classpath named - * {@code org/springframework/restdocs/templates/{name}.snippet}. If no such - * resource exists an attempt is made to return a default resource that is appropriate for - * the configured snippet format. + * Templates are resolved by looking for resources on the classpath. The following + * locations are checked in order: + *

        + *
      1. + * org/springframework/restdocs/templates/${templateFormatId}/${name}.snippet + *
      2. + *
      3. org/springframework/restdocs/templates/${name}.snippet
      4. + *
      5. + * org/springframework/restdocs/templates/${templateFormatId}/default-${name}.snippet + *
      6. + *
      * * @author Andy Wilkinson + * @see TemplateFormat#getId() */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { @@ -57,18 +65,37 @@ public StandardTemplateResourceResolver(TemplateFormat templateFormat) { @Override public Resource resolveTemplateResource(String name) { - ClassPathResource classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/" + name + ".snippet"); - if (!classPathResource.exists()) { - classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/" - + this.templateFormat.getId() + "/" + name + ".snippet"); - if (!classPathResource.exists()) { - throw new IllegalStateException("Template named '" + name - + "' could not be resolved"); - } + Resource formatSpecificCustomTemplate = getFormatSpecificCustomTemplate(name); + if (formatSpecificCustomTemplate.exists()) { + return formatSpecificCustomTemplate; } - return classPathResource; + Resource customTemplate = getCustomTemplate(name); + if (customTemplate.exists()) { + return customTemplate; + } + Resource defaultTemplate = getDefaultTemplate(name); + if (defaultTemplate.exists()) { + return defaultTemplate; + } + throw new IllegalStateException("Template named '" + name + + "' could not be resolved"); + } + + private Resource getFormatSpecificCustomTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s/%s.snippet", + this.templateFormat.getId(), name)); + } + + private Resource getCustomTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s.snippet", name)); + } + + private Resource getDefaultTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s/default-%s.snippet", + this.templateFormat.getId(), name)); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index 8d8ef578a..1d152335a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -47,10 +47,17 @@ public class StandardTemplateResourceResolverTests { private final TestClassLoader classLoader = new TestClassLoader(); @Test - public void customSnippetResolution() throws Exception { + public void formatSpecificCustomSnippetHasHighestPrecedence() throws Exception { + this.classLoader.addResource( + "org/springframework/restdocs/templates/asciidoctor/test.snippet", + getClass().getResource("test-format-specific-custom.snippet")); this.classLoader.addResource( "org/springframework/restdocs/templates/test.snippet", getClass() - .getResource("test.snippet")); + .getResource("test-custom.snippet")); + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -62,14 +69,42 @@ public Resource call() { }); - assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + assertThat( + snippet.getURL(), + is(equalTo(getClass().getResource("test-format-specific-custom.snippet")))); } @Test - public void fallsBackToDefaultSnippet() throws Exception { + public void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() + throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/test.snippet", - getClass().getResource("test.snippet")); + "org/springframework/restdocs/templates/test.snippet", getClass() + .getResource("test-custom.snippet")); + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); + Resource snippet = doWithThreadContextClassLoader(this.classLoader, + new Callable() { + + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver + .resolveTemplateResource("test"); + } + + }); + + assertThat(snippet.getURL(), + is(equalTo(getClass().getResource("test-custom.snippet")))); + } + + @Test + public void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -81,11 +116,12 @@ public Resource call() { }); - assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + assertThat(snippet.getURL(), + is(equalTo(getClass().getResource("test-default.snippet")))); } @Test - public void failsIfCustomAndDefaultSnippetDoNotExist() throws Exception { + public void failsIfCustomAndDefaultSnippetsDoNotExist() throws Exception { this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage(equalTo("Template named 'test' could not be resolved")); doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-custom.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet rename to spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-custom.snippet diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet new file mode 100644 index 000000000..e69de29bb diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet new file mode 100644 index 000000000..e69de29bb From 0ef93074814d8d6f5ccea38acd5a5e8ed23f2266 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 15:22:15 +0000 Subject: [PATCH 0221/1059] Add support for manually managing the RestDocumentationContext In 1.0, the reliance on JUnit was more widespread than it should have been. It should have been isolated to the JUnit TestRule implementation, RestDocumentation. Unfortunately, RestDocumentation was also an argument to MockMvcRestDocumentation.documentationConfiguration which made it impossible to use Spring REST Docs without having JUnit on the classpath. This commit introduces JUnitRestDocumentation and ManualRestDocumentation. The format is a direct replacement for RestDocumentation which has been reworked to delegate to JUnitRestDocumentation. The latter allows manual management of the RestDocumentationContext, primarily for use with TestNG. A new interface, RestDocumentationContextProvider, has been introduced. It is implemented by RestDocumentation, JUnitRestDocumentation and ManualRestDocumentation. MockMvcRestDocumentation.documentationConfiguration has been overridden to also accept a RestDocumentationContextProvider. The method that accepts a RestDocumentation has been deprecated, as has RestDocumentation itself. The documentation has been updated to encourage the use of JUnitRestDocumentation and a sample illustrating the use of Spring REST Docs with TestNG has been added. Closes gh-171 --- build.gradle | 4 + docs/build.gradle | 2 + docs/src/docs/asciidoc/getting-started.adoc | 73 +++++++- .../mockmvc/CustomDefaultSnippets.java | 9 +- .../com/example/mockmvc/CustomEncoding.java | 7 +- .../com/example/mockmvc/CustomFormat.java | 7 +- .../mockmvc/CustomUriConfiguration.java | 7 +- .../mockmvc/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 58 +++++++ .../mockmvc/ExampleApplicationTests.java | 5 +- .../example/mockmvc/ParameterizedOutput.java | 3 +- .../java/com/example/mockmvc/Payload.java | 2 +- .../restassured/CustomDefaultSnippets.java | 5 +- .../example/restassured/CustomEncoding.java | 3 +- .../com/example/restassured/CustomFormat.java | 3 +- .../restassured/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 54 ++++++ .../restassured/ExampleApplicationTests.java | 3 +- .../restassured/ParameterizedOutput.java | 3 +- .../com/example/notes/ApiDocumentation.java | 6 +- .../notes/GettingStartedDocumentation.java | 6 +- .../com/example/notes/ApiDocumentation.java | 4 +- .../notes/GettingStartedDocumentation.java | 4 +- samples/testng/build.gradle | 62 +++++++ .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/testng/gradlew | 164 ++++++++++++++++++ samples/testng/gradlew.bat | 90 ++++++++++ samples/testng/src/docs/asciidoc/index.adoc | 21 +++ .../testng/SampleTestNgApplication.java | 41 +++++ .../testng/SampleTestNgApplicationTests.java | 68 ++++++++ spring-restdocs-core/build.gradle | 4 +- .../restdocs/JUnitRestDocumentation.java | 70 ++++++++ .../restdocs/ManualRestDocumentation.java | 82 +++++++++ .../restdocs/RestDocumentation.java | 42 +---- .../restdocs/RestDocumentationContext.java | 2 + .../RestDocumentationContextProvider.java | 36 ++++ spring-restdocs-mockmvc/build.gradle | 1 + .../mockmvc/MockMvcRestDocumentation.java | 20 ++- .../MockMvcRestDocumentationConfigurer.java | 20 +-- ...ckMvcRestDocumentationConfigurerTests.java | 4 +- ...kMvcRestDocumentationIntegrationTests.java | 4 +- .../RestAssuredRestDocumentation.java | 10 +- ...estAssuredRestDocumentationConfigurer.java | 11 +- ...suredRestDocumentationConfigurerTests.java | 4 +- ...uredRestDocumentationIntegrationTests.java | 4 +- 46 files changed, 932 insertions(+), 108 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java create mode 100644 samples/testng/build.gradle create mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/testng/gradlew create mode 100644 samples/testng/gradlew.bat create mode 100644 samples/testng/src/docs/asciidoc/index.adoc create mode 100644 samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java create mode 100644 samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java diff --git a/build.gradle b/build.gradle index 75fd5ae99..0e8acc927 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,10 @@ samples { restNotesSpringDataRest { workingDir "$projectDir/samples/rest-notes-spring-data-rest" } + + testNg { + workingDir "$projectDir/samples/testng" + } } task api (type: Javadoc) { diff --git a/docs/build.gradle b/docs/build.gradle index 2816cd284..234d4ea4f 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -6,6 +6,8 @@ dependencies { testCompile project(':spring-restdocs-mockmvc') testCompile project(':spring-restdocs-restassured') testCompile 'javax.validation:validation-api' + testCompile 'junit:junit' + testCompile 'org.testng:testng:6.9.10' } tasks.findByPath("artifactoryPublish")?.enabled = false diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2dc9bc92c..415836231 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -203,7 +203,7 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] === Generating documentation snippets -Spring REST Docs uses JUnit and +Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or http://www.rest-assured.io[REST Assured] to make requests to the service that you are documenting. It then produces documentation snippets for the request and the resulting @@ -214,11 +214,18 @@ response. [[getting-started-documentation-snippets-setup]] ==== Setting up your tests -The first step in generating documentation snippets is to declare a `public` -`RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` -rule is configured with the output directory into which generated snippets should be -written. This output directory should match the snippets directory that you have -configured in your `build.gradle` or `pom.xml` file. +Exactly how you setup your tests depends on the test framework that you're using. +Spring REST Docs provides first-class support for JUnit. Other frameworks, such as TestNG, +are also supported although slightly more setup is required. + +[[getting-started-documentation-snippets-setup-junit]] +===== Setting up your JUnit tests + +When using JUnit, the first step in generating documentation snippets is to declare a +`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. The +`JUnitRestDocumentation` rule is configured with the output directory into which generated +snippets should be written. This output directory should match the snippets directory that +you have configured in your `build.gradle` or `pom.xml` file. For Maven (`pom.xml` that will typically be `target/generated-snippets` and for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: @@ -227,14 +234,16 @@ Gradle (`build.gradle`) it will typically be `build/generated-snippets`: .Maven ---- @Rule -public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); +public JUnitRestDocumentation restDocumentation = + new JUnitRestDocumentation("target/generated-snippets"); ---- [source,java,indent=0,role="secondary"] .Gradle ---- @Rule -public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); +public JUnitRestDocumentation restDocumentation = + new JUnitRestDocumentation("build/generated-snippets"); ---- Next, provide an `@Before` method to configure MockMvc or REST Assured: @@ -263,6 +272,54 @@ configuration. Refer to the <> for more in +===== Setting up your tests without JUnit +[[getting-started-documentation-snippets-setup-manual]] + +The configuration when JUnit is not being used is largely similar to when it is being +used. This section describes the key differences. The {samples}/testng[TestNG sample] also +illustrates the approach. + +The first difference is that `ManualRestDocumentation` should be used in place of +`JUnitRestDocumentation` and there's no need for the `@Rule` annotation: + +[source,java,indent=0,role="primary"] +.Maven +---- +private ManualRestDocumentation restDocumentation = + new ManualRestDocumentation("target/generated-snippets"); +---- + +[source,java,indent=0,role="secondary"] +.Gradle +---- +private ManualRestDocumentation restDocumentation = + new ManualRestDocumentation("build/generated-snippets"); +---- + +Secondly, `ManualRestDocumentation.beforeTest(Class, String)` +must be called before each test. This can be done as part of the method that is +configuring MockMVC or REST Assured: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=setup] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=setup] +---- + +Lastly, `ManualRestDocumentation.afterTest` must be called after each test. For example, +with TestNG: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=teardown] +---- + [[getting-started-documentation-snippets-invoking-the-service]] ==== Invoking the RESTful service diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index fcd6a742a..4be61c375 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -16,21 +16,22 @@ package com.example.mockmvc; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomDefaultSnippets { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 80130ef18..9352ca95e 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -16,20 +16,21 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomEncoding { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 3a32a9b53..2415bd40c 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -16,21 +16,22 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomFormat { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index 9fe6130c9..b091199a8 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -16,20 +16,21 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomUriConfiguration { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index e1f9b9961..85e7e9979 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; @@ -38,7 +39,7 @@ public class EveryTestPreprocessing { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java new file mode 100644 index 000000000..0018c467b --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( + "target/generated-snippets"); + // tag::setup[] + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeMethod + public void setUp(Method method) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] + +} diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 97abfa059..a27725f04 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -29,7 +29,7 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); // tag::setup[] @Autowired @@ -44,4 +44,5 @@ public void setUp() { .build(); } // end::setup[] + } diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index ea6313cd1..c5e6a362a 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -29,7 +30,7 @@ public class ParameterizedOutput { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index bed9663f1..4424b1f31 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -32,7 +32,7 @@ public class Payload { -private MockMvc mockMvc; + private MockMvc mockMvc; public void response() throws Exception { // tag::response[] diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 4166c13e0..ae581cd05 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -29,9 +30,9 @@ public class CustomDefaultSnippets { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); - RequestSpecification spec; + private RequestSpecification spec; @Before public void setUp() { diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index eb85c8059..09039a448 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -28,7 +29,7 @@ public class CustomEncoding { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index c5424ec2a..36ba445d4 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; @@ -29,7 +30,7 @@ public class CustomFormat { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 68b8a88b1..a4ea45880 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.restassured.RestDocumentationFilter; @@ -38,7 +39,7 @@ public class EveryTestPreprocessing { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java new file mode 100644 index 000000000..a28e54aec --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import java.lang.reflect.Method; + +import org.springframework.restdocs.ManualRestDocumentation; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( + "build/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + @BeforeMethod + public void setUp(Method method) { + this.spec = new RequestSpecBuilder().addFilter( + documentationConfiguration(this.restDocumentation)) + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index c0943a777..33840cb70 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -28,7 +29,7 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 10fd8be1d..29c24835a 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -29,7 +30,7 @@ public class ParameterizedOutput { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); private RequestSpecification spec; 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 51581811d..cbcaeb3fc 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,8 +20,8 @@ import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -45,7 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Autowired private NoteRepository noteRepository; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 3770023ba..29418eb1c 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,8 +19,8 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -40,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -58,7 +58,7 @@ public class GettingStartedDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Autowired private ObjectMapper objectMapper; 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 4da12b336..90abbdc62 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 @@ -51,7 +51,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; @@ -71,7 +71,7 @@ public class ApiDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); private RestDocumentationResultHandler document; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 60ba09692..9531e5831 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -43,7 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -61,7 +61,7 @@ public class GettingStartedDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); @Autowired private ObjectMapper objectMapper; diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle new file mode 100644 index 000000000..a6eeb77af --- /dev/null +++ b/samples/testng/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +plugins { + id "org.asciidoctor.convert" version "1.5.3" +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile('org.springframework:spring-test') { + exclude group: 'junit', module: 'junit;' + } + testCompile 'org.testng:testng:6.9.10' + testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" +} + +test { + useTestNG() + outputs.dir snippetsDir +} + +asciidoctor { + attributes 'snippets': snippetsDir + inputs.dir snippetsDir + dependsOn test +} + +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2b8cbfe8d --- /dev/null +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 16 12:33:24 BST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/testng/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/testng/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/testng/src/docs/asciidoc/index.adoc b/samples/testng/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..ff513a31a --- /dev/null +++ b/samples/testng/src/docs/asciidoc/index.adoc @@ -0,0 +1,21 @@ += Spring REST Docs TestNG Sample +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +This sample application demonstrates how to use Spring REST Docs with TestNG. +`SampleTestNgApplication` makes a call to a very simple service and produces three +documentation snippets. + +One showing how to make a request using cURL: + +include::{snippets}/sample/curl-request.adoc[] + +One showing the HTTP request: + +include::{snippets}/sample/http-request.adoc[] + +And one showing the HTTP response: + +include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java new file mode 100644 index 000000000..37674b96b --- /dev/null +++ b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.testng; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class SampleTestNgApplication { + + public static void main(String[] args) { + new SpringApplication(SampleTestNgApplication.class).run(args); + } + + @RestController + private static class SampleController { + + @RequestMapping("/") + public String index() { + return "Hello, World"; + } + + } + +} diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java new file mode 100644 index 000000000..b49883b25 --- /dev/null +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2016 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 com.example.testng; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@SpringApplicationConfiguration(classes=SampleTestNgApplication.class) +@WebAppConfiguration +public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { + + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("build/generated-snippets"); + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeMethod + public void setUp(Method method) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + + @Test + public void sample() throws Exception { + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("sample")); + } + +} diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 2ba4b30bb..9f0934756 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -26,14 +26,14 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'junit:junit' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' - optional 'javax.validation:validation-api' optional 'commons-codec:commons-codec' + optional 'javax.validation:validation-api' + optional 'junit:junit' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java new file mode 100644 index 000000000..67bd5c0d6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014-2016 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; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A JUnit {@link TestRule} used to automatically manage the + * {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public class JUnitRestDocumentation implements RestDocumentationContextProvider, TestRule { + + private final ManualRestDocumentation delegate; + + /** + * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets + * to the given {@code outputDirectory}. + * + * @param outputDirectory the output directory + */ + public JUnitRestDocumentation(String outputDirectory) { + this.delegate = new ManualRestDocumentation(outputDirectory); + } + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + Class testClass = description.getTestClass(); + String methodName = description.getMethodName(); + JUnitRestDocumentation.this.delegate.beforeTest(testClass, methodName); + try { + base.evaluate(); + } + finally { + JUnitRestDocumentation.this.delegate.afterTest(); + } + } + + }; + + } + + @Override + public RestDocumentationContext beforeOperation() { + return this.delegate.beforeOperation(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java new file mode 100644 index 000000000..ff12d5910 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2016 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; + +import java.io.File; + +/** + * {@code ManualRestDocumentation} is used to manually manage the + * {@link RestDocumentationContext}. Primarly intended for use with TestNG, but suitable + * for use in any environment where manual management of the context is required. + *

      + * Users of JUnit should use {@link JUnitRestDocumentation} and take advantage of its + * Rule-based support for automatic management of the context. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public final class ManualRestDocumentation implements RestDocumentationContextProvider { + + private final File outputDirectory; + + private RestDocumentationContext context; + + /** + * Creates a new {@code ManualRestDocumentation} instance that will generate snippets + * to the given {@code outputDirectory}. + * + * @param outputDirectory the output directory + */ + public ManualRestDocumentation(String outputDirectory) { + this.outputDirectory = new File(outputDirectory); + } + + /** + * Notification that a test is about to begin. Creates a + * {@link RestDocumentationContext} for the test on the given {@code testClass} with + * the given {@code testMethodName}. Must be followed by a call to + * {@link #afterTest()} once the test has completed. + * + * @param testClass the test class + * @param testMethodName the name of the test method + * @throws IllegalStateException if a context has already be created + */ + @SuppressWarnings("deprecation") + public void beforeTest(Class testClass, String testMethodName) { + if (this.context != null) { + throw new IllegalStateException( + "Context already exists. Did you forget to call afterTest()?"); + } + this.context = new RestDocumentationContext(testClass, testMethodName, + this.outputDirectory); + } + + /** + * Notification that a test has completed. Clears the {@link RestDocumentationContext} + * that was previously established by a call to {@link #beforeTest(Class, String)}. + */ + public void afterTest() { + this.context = null; + } + + @Override + public RestDocumentationContext beforeOperation() { + this.context.getAndIncrementStepCount(); + return this.context; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java index 401b67139..51c96aca2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -16,8 +16,6 @@ package org.springframework.restdocs; -import java.io.File; - import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -27,13 +25,12 @@ * JUnit tests. * * @author Andy Wilkinson - * + * @deprecated Since 1.1 in favor of {@link JUnitRestDocumentation} */ -public class RestDocumentation implements TestRule { - - private final String outputDirectory; +@Deprecated +public class RestDocumentation implements TestRule, RestDocumentationContextProvider { - private RestDocumentationContext context; + private final JUnitRestDocumentation delegate; /** * Creates a new {@code RestDocumentation} instance that will generate snippets to the @@ -42,40 +39,17 @@ public class RestDocumentation implements TestRule { * @param outputDirectory the output directory */ public RestDocumentation(String outputDirectory) { - this.outputDirectory = outputDirectory; + this.delegate = new JUnitRestDocumentation(outputDirectory); } @Override public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - Class testClass = description.getTestClass(); - String methodName = description.getMethodName(); - RestDocumentation.this.context = new RestDocumentationContext(testClass, - methodName, new File(RestDocumentation.this.outputDirectory)); - try { - base.evaluate(); - } - finally { - RestDocumentation.this.context = null; - } - } - - }; - + return this.delegate.apply(base, description); } - /** - * Notification that a RESTful operation that should be documented is about to be - * performed. Returns a {@link RestDocumentationContext} for the operation. - * - * @return the context for the operation - */ + @Override public RestDocumentationContext beforeOperation() { - this.context.getAndIncrementStepCount(); - return this.context; + return this.delegate.beforeOperation(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java index 86dbf6c8e..d76fc2203 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java @@ -43,7 +43,9 @@ public final class RestDocumentationContext { * @param testClass the class whose test is being executed * @param testMethodName the name of the test method that is being executed * @param outputDirectory the directory to which documentation should be written. + * @deprecated Since 1.1 in favor of {@link ManualRestDocumentation}. */ + @Deprecated public RestDocumentationContext(Class testClass, String testMethodName, File outputDirectory) { this.testClass = testClass; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java new file mode 100644 index 000000000..5dbe7a78c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2016 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; + +/** + * A {@code RestDocumentationContextProvider} is used to provide access to the + * {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public interface RestDocumentationContextProvider { + + /** + * Returns a {@link RestDocumentationContext} for the operation that is about to be + * performed. + * + * @return the context for the operation + */ + RestDocumentationContext beforeOperation(); + +} diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index ba3ab171b..b93fba78d 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -3,6 +3,7 @@ description = 'Spring REST Docs MockMvc' dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') + optional 'junit:junit' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index cf1840cbe..070668fd8 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; @@ -31,6 +32,7 @@ * * @author Andy Wilkinson */ +@SuppressWarnings("deprecation") public abstract class MockMvcRestDocumentation { private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); @@ -48,10 +50,26 @@ private MockMvcRestDocumentation() { * @param restDocumentation the REST documentation * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * @deprecated Since 1.1 in favor of + * {@link #documentationConfiguration(RestDocumentationContextProvider)} */ + @Deprecated public static MockMvcRestDocumentationConfigurer documentationConfiguration( RestDocumentation restDocumentation) { - return new MockMvcRestDocumentationConfigurer(restDocumentation); + return documentationConfiguration((RestDocumentationContextProvider) restDocumentation); + } + + /** + * Provides access to a {@link MockMvcConfigurer} that can be used to configure a + * {@link MockMvc} instance using the given {@code contextProvider}. + * + * @param contextProvider the context provider + * @return the configurer + * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + */ + public static MockMvcRestDocumentationConfigurer documentationConfiguration( + RestDocumentationContextProvider contextProvider) { + return new MockMvcRestDocumentationConfigurer(contextProvider); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index e20ecd01b..bbe92f987 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -20,8 +20,8 @@ import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -44,11 +44,10 @@ public class MockMvcRestDocumentationConfigurer private final UriConfigurer uriConfigurer = new UriConfigurer(this); - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextManager; - MockMvcRestDocumentationConfigurer(RestDocumentation restDocumentation) { - super(); - this.restDocumentation = restDocumentation; + MockMvcRestDocumentationConfigurer(RestDocumentationContextProvider contextManager) { + this.contextManager = contextManager; } /** @@ -64,7 +63,7 @@ public UriConfigurer uris() { @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation); + return new ConfigurerApplyingRequestPostProcessor(this.contextManager); } @Override @@ -80,15 +79,16 @@ public MockMvcSnippetConfigurer snippets() { private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextManager; - private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentation) { - this.restDocumentation = restDocumentation; + private ConfigurerApplyingRequestPostProcessor( + RestDocumentationContextProvider contextManager) { + this.contextManager = contextManager; } @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - RestDocumentationContext context = this.restDocumentation.beforeOperation(); + RestDocumentationContext context = this.contextManager.beforeOperation(); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); configuration diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index ad6ac2691..bcf2dd05a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -43,7 +43,7 @@ public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @Rule - public RestDocumentation restDocumentation = new RestDocumentation("test"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("test"); @Test public void defaultConfiguration() { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ac83d9719..368f96018 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; import org.springframework.test.context.ContextConfiguration; @@ -96,7 +96,7 @@ public class MockMvcRestDocumentationIntegrationTests { @Rule - public RestDocumentation restDocumentation = new RestDocumentation( + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); @Autowired diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 6f2c3f4ce..b9c53742f 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.restassured; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; @@ -104,14 +104,14 @@ public static RestDocumentationFilter document(String identifier, /** * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be - * used to configure Spring REST Docs using the given {@code restDocumentation}. + * used to configure Spring REST Docs using the given {@code contextProvider}. * - * @param restDocumentation the REST documentation + * @param contextProvider the context provider * @return the configurer */ public static RestAssuredRestDocumentationConfigurer documentationConfiguration( - RestDocumentation restDocumentation) { - return new RestAssuredRestDocumentationConfigurer(restDocumentation); + RestDocumentationContextProvider contextProvider) { + return new RestAssuredRestDocumentationConfigurer(contextProvider); } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 4702a752d..f655bb95e 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -19,8 +19,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.config.RestDocumentationConfigurer; import com.jayway.restassured.filter.Filter; @@ -42,10 +42,11 @@ public final class RestAssuredRestDocumentationConfigurer private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( this); - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextProvider; - RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { - this.restDocumentation = restDocumentation; + RestAssuredRestDocumentationConfigurer( + RestDocumentationContextProvider contextProvider) { + this.contextProvider = contextProvider; } @Override @@ -56,7 +57,7 @@ public RestAssuredSnippetConfigurer snippets() { @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) { - RestDocumentationContext context = this.restDocumentation.beforeOperation(); + RestDocumentationContext context = this.contextProvider.beforeOperation(); filterContext.setValue(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 6b4e02c68..81ed3cee5 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -22,7 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; @@ -48,7 +48,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index ff3084d84..6717f8c60 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -88,7 +88,7 @@ public class RestAssuredRestDocumentationIntegrationTests { @Rule - public RestDocumentation restDocumentation = new RestDocumentation( + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); @Value("${local.server.port}") From 74f9e272fd5af48c30256d4bf6f3e4fc5abe2eb8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 Feb 2016 20:57:53 +0000 Subject: [PATCH 0222/1059] Apply Eclipse Mars' code formatting --- config/checkstyle/checkstyle.xml | 2 +- config/eclipse/org.eclipse.jdt.core.prefs | 4 +- config/eclipse/org.eclipse.jdt.ui.prefs | 4 +- .../restdocs/config/package-info.java | 1 - .../constraints/ConstraintDescriptions.java | 7 +- ...ceBundleConstraintDescriptionResolver.java | 12 +- .../ValidatorConstraintResolver.java | 7 +- .../restdocs/constraints/package-info.java | 1 - .../restdocs/curl/CurlRequestSnippet.java | 14 +- .../restdocs/curl/QueryStringParser.java | 8 +- .../restdocs/curl/package-info.java | 1 - .../restdocs/http/HttpRequestSnippet.java | 13 +- .../restdocs/http/package-info.java | 1 - .../hypermedia/AbstractJsonLinkExtractor.java | 8 +- .../hypermedia/HypermediaDocumentation.java | 2 +- .../restdocs/hypermedia/Link.java | 4 +- .../restdocs/hypermedia/LinksSnippet.java | 10 +- .../restdocs/hypermedia/package-info.java | 1 - .../operation/AbstractOperationMessage.java | 4 +- .../operation/OperationRequestFactory.java | 11 +- .../operation/OperationResponseFactory.java | 14 +- .../operation/StandardOperationRequest.java | 4 +- .../StandardOperationRequestPart.java | 4 +- .../operation/StandardOperationResponse.java | 4 +- .../restdocs/operation/package-info.java | 5 +- ...ContentModifyingOperationPreprocessor.java | 4 +- .../LinkMaskingContentModifier.java | 4 +- .../operation/preprocess/Preprocessors.java | 10 +- .../PrettyPrintingContentModifier.java | 21 +- .../operation/preprocess/package-info.java | 1 - .../restdocs/package-info.java | 1 - .../payload/AbstractFieldsSnippet.java | 7 +- .../restdocs/payload/JsonContentHandler.java | 8 +- .../restdocs/payload/JsonFieldPath.java | 4 +- .../restdocs/payload/JsonFieldProcessor.java | 20 +- .../restdocs/payload/XmlContentHandler.java | 13 +- .../restdocs/payload/package-info.java | 1 - .../request/AbstractParametersSnippet.java | 9 +- .../request/PathParametersSnippet.java | 4 +- .../restdocs/request/package-info.java | 1 - .../restdocs/snippet/IgnorableDescriptor.java | 4 +- ...cumentationContextPlaceholderResolver.java | 5 +- .../snippet/StandardWriterResolver.java | 6 +- .../restdocs/snippet/TemplatedSnippet.java | 8 +- .../restdocs/snippet/package-info.java | 1 - .../StandardTemplateResourceResolver.java | 7 +- .../mustache/MustacheTemplateEngine.java | 4 +- .../templates/mustache/package-info.java | 1 - .../restdocs/templates/package-info.java | 1 - .../ConstraintDescriptionsTests.java | 3 +- ...dleConstraintDescriptionResolverTests.java | 59 ++-- .../ValidatorConstraintResolverTests.java | 6 +- .../curl/CurlRequestSnippetTests.java | 279 +++++++-------- .../restdocs/curl/QueryStringParserTests.java | 21 +- .../headers/RequestHeadersSnippetTests.java | 139 ++++---- .../headers/ResponseHeadersSnippetTests.java | 138 ++++---- .../http/HttpRequestSnippetTests.java | 196 ++++++----- .../http/HttpResponseSnippetTests.java | 64 ++-- .../ContentTypeLinkExtractorTests.java | 5 +- .../LinkExtractorsPayloadTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 128 ++++--- ...ntModifyingOperationPreprocessorTests.java | 1 + ...tingOperationRequestPreprocessorTests.java | 11 +- ...ingOperationResponsePreprocessorTests.java | 15 +- ...derRemovingOperationPreprocessorTests.java | 1 + .../LinkMaskingContentModifierTests.java | 20 +- .../PatternReplacingContentModifierTests.java | 6 +- .../PrettyPrintingContentModifierTests.java | 21 +- .../payload/JsonFieldProcessorTests.java | 34 +- .../payload/JsonFieldTypeResolverTests.java | 22 +- .../payload/RequestFieldsSnippetTests.java | 261 +++++++------- .../payload/ResponseFieldsSnippetTests.java | 332 ++++++++++-------- .../request/PathParametersSnippetTests.java | 132 ++++--- .../RequestParametersSnippetTests.java | 119 ++++--- ...tationContextPlaceholderResolverTests.java | 14 +- .../snippet/StandardWriterResolverTests.java | 25 +- .../snippet/TemplatedSnippetTests.java | 7 +- ...StandardTemplateResourceResolverTests.java | 9 +- .../restdocs/test/ExpectedSnippet.java | 5 +- .../restdocs/test/OperationBuilder.java | 9 +- .../restdocs/test/SnippetMatchers.java | 34 +- .../MockMvcOperationRequestFactory.java | 30 +- .../RestDocumentationMockMvcConfigurer.java | 15 +- .../RestDocumentationRequestBuilders.java | 32 +- .../RestDocumentationResultHandler.java | 20 +- .../restdocs/mockmvc/SnippetConfigurer.java | 9 +- .../restdocs/mockmvc/UriConfigurer.java | 4 +- .../restdocs/mockmvc/package-info.java | 1 - .../MockMvcOperationRequestFactoryTests.java | 38 +- ...kMvcRestDocumentationIntegrationTests.java | 170 ++++----- .../RestDocumentationConfigurerTests.java | 17 +- ...RestDocumentationRequestBuildersTests.java | 1 + 92 files changed, 1450 insertions(+), 1298 deletions(-) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b7908d746..d9cdead77 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -81,7 +81,7 @@ - + diff --git a/config/eclipse/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs index 462feac8f..62ded11bf 100644 --- a/config/eclipse/org.eclipse.jdt.core.prefs +++ b/config/eclipse/org.eclipse.jdt.core.prefs @@ -10,7 +10,6 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.7 @@ -384,8 +383,9 @@ org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true org.eclipse.jdt.core.formatter.tabulation.char=tab org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_on_off_tags=true org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.springframework.ide.eclipse.jdt.formatter.javaformatter diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index 681d02ad0..429b30236 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -62,9 +62,9 @@ editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_Spring REST Docs Java Conventions formatter_settings_version=12 org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false +org.eclipse.jdt.ui.gettersetter.use.is=true org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; +org.eclipse.jdt.ui.importorder=java;javax;;org.springframework;\#; org.eclipse.jdt.ui.javadoc=true org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java index f338eae83..1b4a13e01 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java @@ -18,4 +18,3 @@ * Classes for configuring Spring REST Docs. */ package org.springframework.restdocs.config; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java index 7fc11026d..c55857980 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java @@ -56,7 +56,8 @@ public ConstraintDescriptions(Class clazz) { * @param constraintResolver the constraint resolver */ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResolver) { - this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver()); + this(clazz, constraintResolver, + new ResourceBundleConstraintDescriptionResolver()); } /** @@ -95,8 +96,8 @@ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResol * @return the list of constraint descriptions */ public List descriptionsForProperty(String property) { - List constraints = this.constraintResolver.resolveForProperty( - property, this.clazz); + List constraints = this.constraintResolver + .resolveForProperty(property, this.clazz); List descriptions = new ArrayList<>(); for (Constraint constraint : constraints) { descriptions.add(this.descriptionResolver.resolveDescription(constraint)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 660ed4b5f..759950a64 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -64,8 +64,8 @@ * * @author Andy Wilkinson */ -public class ResourceBundleConstraintDescriptionResolver implements - ConstraintDescriptionResolver { +public class ResourceBundleConstraintDescriptionResolver + implements ConstraintDescriptionResolver { private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "${", "}"); @@ -99,8 +99,8 @@ private static ResourceBundle getBundle(String name) { try { return ResourceBundle.getBundle( ResourceBundleConstraintDescriptionResolver.class.getPackage() - .getName() + "." + name, Locale.getDefault(), Thread - .currentThread().getContextClassLoader()); + .getName() + "." + name, + Locale.getDefault(), Thread.currentThread().getContextClassLoader()); } catch (MissingResourceException ex) { return null; @@ -126,8 +126,8 @@ private String getDescription(String key) { return this.defaultDescriptions.getString(key); } - private static final class ConstraintPlaceholderResolver implements - PlaceholderResolver { + private static final class ConstraintPlaceholderResolver + implements PlaceholderResolver { private final Constraint constraint; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index 664691c65..531157b2a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -70,10 +70,9 @@ public List resolveForProperty(String property, Class clazz) { if (propertyDescriptor != null) { for (ConstraintDescriptor constraintDescriptor : propertyDescriptor .getConstraintDescriptors()) { - constraints - .add(new Constraint(constraintDescriptor.getAnnotation() - .annotationType().getName(), constraintDescriptor - .getAttributes())); + constraints.add(new Constraint( + constraintDescriptor.getAnnotation().annotationType().getName(), + constraintDescriptor.getAttributes())); } } return constraints; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java index 9fc48616d..fb1e842ec 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java @@ -18,4 +18,3 @@ * Documenting a RESTful API's constraints. */ package org.springframework.restdocs.constraints; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 3e903f377..4f4ea07b7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -48,6 +48,7 @@ public class CurlRequestSnippet extends TemplatedSnippet { private static final Set HEADER_FILTERS; + static { Set headerFilters = new HashSet<>(); headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); @@ -102,7 +103,8 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter writer) { + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); @@ -110,7 +112,8 @@ private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter wr } } - private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { + private void writeHttpMethodIfNecessary(OperationRequest request, + PrintWriter writer) { if (!HttpMethod.GET.equals(request.getMethod())) { writer.print(String.format(" -X %s", request.getMethod())); } @@ -145,8 +148,8 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) writer.printf("@%s", part.getSubmittedFileName()); } if (part.getHeaders().getContentType() != null) { - writer.append(";type=").append( - part.getHeaders().getContentType().toString()); + writer.append(";type=") + .append(part.getHeaders().getContentType().toString()); } writer.append("'"); @@ -170,7 +173,8 @@ else if (isPutOrPost(request)) { } } - private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { Parameters uniqueParameters = getUniqueParameters(request); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java index d09d6e441..0982adf95 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -64,8 +64,8 @@ private void processParameter(String parameter, Parameters parameters) { parameters.add(decode(name), decode(value)); } else { - throw new IllegalArgumentException("The parameter '" + parameter - + "' is malformed"); + throw new IllegalArgumentException( + "The parameter '" + parameter + "' is malformed"); } } @@ -74,8 +74,8 @@ private String decode(String encoded) { return URLDecoder.decode(encoded, "UTF-8"); } catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + encoded - + " using UTF-8", ex); + throw new IllegalStateException( + "Unable to URL encode " + encoded + " using UTF-8", ex); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java index 85ee026af..7c07b91e0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -18,4 +18,3 @@ * Documenting the curl command required to make a request to a RESTful API. */ package org.springframework.restdocs.curl; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 26d209fab..38d1e35f6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -66,12 +66,9 @@ protected HttpRequestSnippet(Map attributes) { protected Map createModel(Operation operation) { Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); - model.put( - "path", - operation.getRequest().getUri().getRawPath() - + (StringUtils.hasText(operation.getRequest().getUri() - .getRawQuery()) ? "?" - + operation.getRequest().getUri().getRawQuery() : "")); + model.put("path", operation.getRequest().getUri().getRawPath() + + (StringUtils.hasText(operation.getRequest().getUri().getRawQuery()) + ? "?" + operation.getRequest().getUri().getRawQuery() : "")); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; @@ -149,8 +146,8 @@ private void writePartBoundary(PrintWriter writer) { } private void writePart(OperationRequestPart part, PrintWriter writer) { - writePart(part.getName(), part.getContentAsString(), part.getHeaders() - .getContentType(), writer); + writePart(part.getName(), part.getContentAsString(), + part.getHeaders().getContentType(), writer); } private void writePart(String name, String value, MediaType contentType, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java index 5d3206012..da609abbe 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java @@ -19,4 +19,3 @@ * returned. */ package org.springframework.restdocs.http; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index cc9f115ad..a345cd2de 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -20,10 +20,10 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.operation.OperationResponse; - import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.restdocs.operation.OperationResponse; + /** * Abstract base class for a {@link LinkExtractor} that extracts links from JSON. * @@ -37,8 +37,8 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor { @SuppressWarnings("unchecked") public Map> extractLinks(OperationResponse response) throws IOException { - Map jsonContent = this.objectMapper.readValue( - response.getContent(), Map.class); + Map jsonContent = this.objectMapper + .readValue(response.getContent(), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index a4fbf42d4..d5ddaf0b4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -170,7 +170,7 @@ public static LinkExtractor halLinks() { * "href": "http://example.com/foo" * } * ] - * } + * } * * * @return The extractor for Atom-style links diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index eed9dfb4c..112886f0b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -88,8 +88,8 @@ public boolean equals(Object obj) { @Override public String toString() { - return new ToStringCreator(this).append("rel", this.rel) - .append("href", this.href).toString(); + return new ToStringCreator(this).append("rel", this.rel).append("href", this.href) + .toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index b7f868c85..771e2e7a8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -56,7 +56,8 @@ public class LinksSnippet extends TemplatedSnippet { * @param linkExtractor the link extractor * @param descriptors the link descriptors */ - protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { + protected LinksSnippet(LinkExtractor linkExtractor, + List descriptors) { this(linkExtractor, descriptors, null); } @@ -76,9 +77,10 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), "The descriptor for link '" - + descriptor.getRel() + "' must either have a description or be" - + " marked as " + "ignored"); + Assert.notNull(descriptor.getDescription(), + "The descriptor for link '" + descriptor.getRel() + + "' must either have a description or be" + " marked as " + + "ignored"); } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java index c8d3dab75..0d3ad26d2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java @@ -18,4 +18,3 @@ * Documenting a RESTful API that uses hypermedia. */ package org.springframework.restdocs.hypermedia; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index c5ede617d..41e8fec3e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -49,8 +49,8 @@ public HttpHeaders getHeaders() { public String getContentAsString() { if (this.content.length > 0) { Charset charset = extractCharsetFromContentTypeHeader(); - return charset != null ? new String(this.content, charset) : new String( - this.content); + return charset != null ? new String(this.content, charset) + : new String(this.content); } return ""; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 693be1ddb..c105fd75c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -45,8 +45,8 @@ public class OperationRequestFactory { public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { - return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, - uri, content), parameters, parts); + return new StandardOperationRequest(uri, method, content, + augmentHeaders(headers, uri, content), parameters, parts); } /** @@ -74,7 +74,8 @@ public OperationRequest createFrom(OperationRequest original, byte[] newContent) * * @return The new request with the new content */ - public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { + public OperationRequest createFrom(OperationRequest original, + HttpHeaders newHeaders) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, original.getParameters(), original.getParts()); @@ -89,8 +90,8 @@ private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( - updatedContent).getHeaders(); + return new HttpHeadersHelper(originalHeaders) + .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index c3417c9f8..1cfd1434a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -36,7 +36,8 @@ public class OperationResponseFactory { * @param content the content of the request * @return the {@code OperationResponse} */ - public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) { + public OperationResponse create(HttpStatus status, HttpHeaders headers, + byte[] content) { return new StandardOperationResponse(status, augmentHeaders(headers, content), content); } @@ -53,8 +54,8 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] c * @return The new response with the new content */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { - return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders( - original.getHeaders(), newContent), newContent); + return new StandardOperationResponse(original.getStatus(), + getUpdatedHeaders(original.getHeaders(), newContent), newContent); } /** @@ -66,7 +67,8 @@ public OperationResponse createFrom(OperationResponse original, byte[] newConten * * @return The new response with the new headers */ - public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { + public OperationResponse createFrom(OperationResponse original, + HttpHeaders newHeaders) { return new StandardOperationResponse(original.getStatus(), newHeaders, original.getContent()); } @@ -78,8 +80,8 @@ private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( - updatedContent).getHeaders(); + return new HttpHeadersHelper(originalHeaders) + .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 305d7b2a7..a09bd645f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -28,8 +28,8 @@ * * @author Andy Wilkinson */ -class StandardOperationRequest extends AbstractOperationMessage implements - OperationRequest { +class StandardOperationRequest extends AbstractOperationMessage + implements OperationRequest { private HttpMethod method; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index 8ea231a4d..e9fee9d75 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -23,8 +23,8 @@ * * @author Andy Wilkinson */ -class StandardOperationRequestPart extends AbstractOperationMessage implements - OperationRequestPart { +class StandardOperationRequestPart extends AbstractOperationMessage + implements OperationRequestPart { private final String name; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 32dfa57f7..0131a4b5a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -24,8 +24,8 @@ * * @author Andy Wilkinson */ -class StandardOperationResponse extends AbstractOperationMessage implements - OperationResponse { +class StandardOperationResponse extends AbstractOperationMessage + implements OperationResponse { private final HttpStatus status; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java index d7786bca2..e9ba93d90 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java @@ -15,8 +15,7 @@ */ /** - * Operation API that describes a request that was sent and the response that was - * received when calling a RESTful API. + * Operation API that describes a request that was sent and the response that was received + * when calling a RESTful API. */ package org.springframework.restdocs.operation; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 60c704aa6..d18948e37 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -54,8 +54,8 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { - byte[] modifiedContent = this.contentModifier.modifyContent( - response.getContent(), response.getHeaders().getContentType()); + byte[] modifiedContent = this.contentModifier.modifyContent(response.getContent(), + response.getHeaders().getContentType()); return this.responseFactory.createFrom(response, modifiedContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 9af9c03f3..928f700f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -29,8 +29,8 @@ class LinkMaskingContentModifier implements ContentModifier { private static final String DEFAULT_MASK = "..."; - private static final Pattern LINK_HREF = Pattern.compile( - "\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); + private static final Pattern LINK_HREF = Pattern.compile("\"href\"\\s*:\\s*\"(.*?)\"", + Pattern.DOTALL); private final ContentModifier contentModifier; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index b4aa590dd..35b5c1fd6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -90,7 +90,8 @@ public static OperationPreprocessor removeHeaders(String... headersToRemove) { * @return the preprocessor */ public static OperationPreprocessor maskLinks() { - return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier()); + return new ContentModifyingOperationPreprocessor( + new LinkMaskingContentModifier()); } /** @@ -101,8 +102,8 @@ public static OperationPreprocessor maskLinks() { * @return the preprocessor */ public static OperationPreprocessor maskLinks(String mask) { - return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier( - mask)); + return new ContentModifyingOperationPreprocessor( + new LinkMaskingContentModifier(mask)); } /** @@ -114,7 +115,8 @@ public static OperationPreprocessor maskLinks(String mask) { * @param replacement the replacement * @return the preprocessor */ - public static OperationPreprocessor replacePattern(Pattern pattern, String replacement) { + public static OperationPreprocessor replacePattern(Pattern pattern, + String replacement) { return new ContentModifyingOperationPreprocessor( new PatternReplacingContentModifier(pattern, replacement)); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 9665fd09c..b722038ec 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -34,15 +34,15 @@ import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; -import org.springframework.http.MediaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.http.MediaType; /** * A {@link ContentModifier} that modifies the content by pretty printing it. @@ -52,8 +52,8 @@ public class PrettyPrintingContentModifier implements ContentModifier { private static final List PRETTY_PRINTERS = Collections - .unmodifiableList(Arrays.asList(new JsonPrettyPrinter(), - new XmlPrettyPrinter())); + .unmodifiableList( + Arrays.asList(new JsonPrettyPrinter(), new XmlPrettyPrinter())); @Override public byte[] modifyContent(byte[] originalContent, MediaType contentType) { @@ -98,8 +98,8 @@ private SAXSource createSaxSource(byte[] original) SAXParser parser = parserFactory.newSAXParser(); XMLReader xmlReader = parser.getXMLReader(); xmlReader.setErrorHandler(new SilentErrorHandler()); - return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream( - original))); + return new SAXSource(xmlReader, + new InputSource(new ByteArrayInputStream(original))); } private static final class SilentErrorListener implements ErrorListener { @@ -111,7 +111,8 @@ public void warning(TransformerException exception) } @Override - public void error(TransformerException exception) throws TransformerException { + public void error(TransformerException exception) + throws TransformerException { // Suppress } @@ -146,8 +147,8 @@ private static final class JsonPrettyPrinter implements PrettyPrinter { @Override public String prettyPrint(byte[] original) throws IOException { - ObjectMapper objectMapper = new ObjectMapper().configure( - SerializationFeature.INDENT_OUTPUT, true); + ObjectMapper objectMapper = new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true); return objectMapper.writeValueAsString(objectMapper.readTree(original)); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java index 55913531d..a00ceb0e5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java @@ -18,4 +18,3 @@ * Support for preprocessing an operation prior to it being documented. */ package org.springframework.restdocs.operation.preprocess; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java index 2d3767b2d..73fee6695 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java @@ -18,4 +18,3 @@ * Core Spring REST Docs classes. */ package org.springframework.restdocs; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 3257be17c..dcff13bdd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -57,9 +57,10 @@ protected AbstractFieldsSnippet(String type, List descriptors, for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), "The descriptor for field '" - + descriptor.getPath() + "' must either have a description or" - + " be marked as " + "ignored"); + Assert.notNull(descriptor.getDescription(), + "The descriptor for field '" + descriptor.getPath() + + "' must either have a description or" + " be marked as " + + "ignored"); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index b08f14279..779b75fbc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -44,13 +44,13 @@ class JsonContentHandler implements ContentHandler { } @Override - public List findMissingFields(List fieldDescriptors) { + public List findMissingFields( + List fieldDescriptors) { List missingFields = new ArrayList<>(); Object payload = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional() - && !this.fieldProcessor.hasField( - JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { + if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField( + JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { missingFields.add(fieldDescriptor); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index 3eefe20c1..10478a839 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -87,8 +87,8 @@ private static List extractSegments(String path) { List segments = new ArrayList<>(); while (matcher.find()) { if (previous != matcher.start()) { - segments.addAll(extractDotSeparatedSegments(path.substring(previous, - matcher.start()))); + segments.addAll(extractDotSeparatedSegments( + path.substring(previous, matcher.start()))); } if (matcher.group(1) != null) { segments.add(matcher.group(1)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 5e4ed29d8..fb63a2148 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -89,26 +89,30 @@ else if (context.getPayload() instanceof Map } } - private void handleListPayload(ProcessingContext context, MatchCallback matchCallback) { + private void handleListPayload(ProcessingContext context, + MatchCallback matchCallback) { List list = context.getPayload(); final Iterator items = list.iterator(); if (context.isLeaf()) { while (items.hasNext()) { Object item = items.next(); - matchCallback.foundMatch(new ListMatch(items, list, item, context - .getParentMatch())); + matchCallback.foundMatch( + new ListMatch(items, list, item, context.getParentMatch())); } } else { while (items.hasNext()) { Object item = items.next(); - traverse(context.descend(item, new ListMatch(items, list, item, - context.parent)), matchCallback); + traverse( + context.descend(item, + new ListMatch(items, list, item, context.parent)), + matchCallback); } } } - private void handleMapPayload(ProcessingContext context, MatchCallback matchCallback) { + private void handleMapPayload(ProcessingContext context, + MatchCallback matchCallback) { Map map = context.getPayload(); Object item = map.get(context.getSegment()); MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), @@ -238,8 +242,8 @@ private Match getParentMatch() { } private ProcessingContext descend(Object payload, Match match) { - return new ProcessingContext(payload, this.path, this.segments.subList(1, - this.segments.size()), match); + return new ProcessingContext(payload, this.path, + this.segments.subList(1, this.segments.size()), match); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 798013736..67e2763d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -63,7 +63,8 @@ class XmlContentHandler implements ContentHandler { } @Override - public List findMissingFields(List fieldDescriptors) { + public List findMissingFields( + List fieldDescriptors) { List missingFields = new ArrayList<>(); Document payload = readPayload(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { @@ -78,7 +79,8 @@ public List findMissingFields(List fieldDescri return missingFields; } - private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) { + private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, + Document payload) { try { return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload, XPathConstants.NODESET); @@ -90,15 +92,16 @@ private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document pay private Document readPayload() { try { - return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream( - this.rawContent))); + return this.documentBuilder + .parse(new InputSource(new ByteArrayInputStream(this.rawContent))); } catch (Exception ex) { throw new PayloadHandlingException(ex); } } - private XPathExpression createXPath(String fieldPath) throws XPathExpressionException { + private XPathExpression createXPath(String fieldPath) + throws XPathExpressionException { return XPathFactory.newInstance().newXPath().compile(fieldPath); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java index f306a0191..2c3281d6c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java @@ -18,4 +18,3 @@ * Documenting the payload of a RESTful API's requests and responses. */ package org.springframework.restdocs.payload; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 7eb6e11c1..0be930228 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -54,7 +54,8 @@ protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name"); + Assert.notNull(descriptor.getName(), + "Parameter descriptors must have a name"); if (!descriptor.isIgnored()) { Assert.notNull(descriptor.getDescription(), "The descriptor for parameter '" + descriptor.getName() @@ -71,7 +72,8 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); - for (Entry entry : this.descriptorsByName.entrySet()) { + for (Entry entry : this.descriptorsByName + .entrySet()) { ParameterDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { parameters.add(createModelForDescriptor(descriptor)); @@ -131,7 +133,8 @@ protected final Map getFieldDescriptors() { * @param descriptor the descriptor * @return the model */ - protected Map createModelForDescriptor(ParameterDescriptor descriptor) { + protected Map createModelForDescriptor( + ParameterDescriptor descriptor) { Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 5df99f6f6..4186faa08 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -90,8 +90,8 @@ protected Set extractActualParameters(Operation operation) { } private String extractUrlTemplate(Operation operation) { - String urlTemplate = (String) operation.getAttributes().get( - "org.springframework.restdocs.urlTemplate"); + String urlTemplate = (String) operation.getAttributes() + .get("org.springframework.restdocs.urlTemplate"); Assert.notNull(urlTemplate, "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " + "build the request?"); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java index 744aa7f0e..c2828b7b5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java @@ -18,4 +18,3 @@ * Documenting query and path parameters of requests sent to a RESTful API. */ package org.springframework.restdocs.request; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java index 8174c3815..477431ee5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java @@ -22,8 +22,8 @@ * @param the type of the descriptor * @author Andy Wilkinson */ -public abstract class IgnorableDescriptor> extends - AbstractDescriptor { +public abstract class IgnorableDescriptor> + extends AbstractDescriptor { private boolean ignored = false; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 6c3e00be2..81898c250 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -137,8 +137,9 @@ private String camelCaseToSeparator(String string, String separator) { Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); StringBuffer result = new StringBuffer(); while (matcher.find()) { - String replacement = (matcher.start() > 0) ? separator - + matcher.group(1).toLowerCase() : matcher.group(1).toLowerCase(); + String replacement = (matcher.start() > 0) + ? separator + matcher.group(1).toLowerCase() + : matcher.group(1).toLowerCase(); matcher.appendReplacement(result, replacement); } matcher.appendTail(result); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1ef51ea4e..883e1c115 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -59,7 +59,8 @@ public Writer resolve(String operationName, String snippetName, if (outputFile != null) { createDirectoriesIfNecessary(outputFile); - return new OutputStreamWriter(new FileOutputStream(outputFile), this.encoding); + return new OutputStreamWriter(new FileOutputStream(outputFile), + this.encoding); } else { return new OutputStreamWriter(System.out, this.encoding); @@ -92,7 +93,8 @@ private File makeRelativeToConfiguredOutputDir(File outputFile, private void createDirectoriesIfNecessary(File outputFile) { File parent = outputFile.getParentFile(); if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IllegalStateException("Failed to create directory '" + parent + "'"); + throw new IllegalStateException( + "Failed to create directory '" + parent + "'"); } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 015d1fe8e..dedfa98a3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -57,10 +57,10 @@ protected TemplatedSnippet(String snippetName, Map attributes) { public void document(Operation operation) throws IOException { RestDocumentationContext context = (RestDocumentationContext) operation .getAttributes().get(RestDocumentationContext.class.getName()); - WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get( - WriterResolver.class.getName()); - try (Writer writer = writerResolver.resolve(operation.getName(), - this.snippetName, context)) { + WriterResolver writerResolver = (WriterResolver) operation.getAttributes() + .get(WriterResolver.class.getName()); + try (Writer writer = writerResolver.resolve(operation.getName(), this.snippetName, + context)) { Map model = createModel(operation); model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java index 9c16a3e25..90a6642b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java @@ -18,4 +18,3 @@ * Snippet generation. */ package org.springframework.restdocs.snippet; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 4e3d38e30..847f6ee85 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -38,10 +38,11 @@ public Resource resolveTemplateResource(String name) { "org/springframework/restdocs/templates/" + name + ".snippet"); if (!classPathResource.exists()) { classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/default-" + name + ".snippet"); + "org/springframework/restdocs/templates/default-" + name + + ".snippet"); if (!classPathResource.exists()) { - throw new IllegalStateException("Template named '" + name - + "' could not be resolved"); + throw new IllegalStateException( + "Template named '" + name + "' could not be resolved"); } } return classPathResource; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index df142045a..3a5acc721 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -54,8 +54,8 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) public Template compileTemplate(String name) throws IOException { Resource templateResource = this.templateResourceResolver .resolveTemplateResource(name); - return new MustacheTemplate(this.compiler.compile(new InputStreamReader( - templateResource.getInputStream()))); + return new MustacheTemplate(this.compiler + .compile(new InputStreamReader(templateResource.getInputStream()))); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java index 244b8d9e5..9f7231ce9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java @@ -18,4 +18,3 @@ * JMustache-based implementation of the template API. */ package org.springframework.restdocs.templates.mustache; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java index cac448dd7..a0a11105a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java @@ -18,4 +18,3 @@ * Template API used to render documentation snippets. */ package org.springframework.restdocs.templates; - diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 6968fccff..a3dbd9abe 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -129,8 +129,7 @@ public void size() { @Test public void sizeList() { - assertThat( - this.constraintDescriptions.descriptionsForProperty("sizeList"), + assertThat(this.constraintDescriptions.descriptionsForProperty("sizeList"), contains("Size must be between 1 and 4 inclusive", "Size must be between 8 and 10 inclusive")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 8f2fa07d3..06334f51e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -70,8 +70,8 @@ public void defaultMessageAssertTrue() { public void defaultMessageDecimalMax() { Map configuration = new HashMap<>(); configuration.put("value", "9.875"); - String description = this.resolver.resolveDescription(new Constraint( - DecimalMax.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(DecimalMax.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at most 9.875"))); } @@ -79,8 +79,8 @@ public void defaultMessageDecimalMax() { public void defaultMessageDecimalMin() { Map configuration = new HashMap<>(); configuration.put("value", "1.5"); - String description = this.resolver.resolveDescription(new Constraint( - DecimalMin.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(DecimalMin.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at least 1.5"))); } @@ -89,16 +89,16 @@ public void defaultMessageDigits() { Map configuration = new HashMap<>(); configuration.put("integer", "2"); configuration.put("fraction", "5"); - String description = this.resolver.resolveDescription(new Constraint(Digits.class - .getName(), configuration)); - assertThat(description, is(equalTo("Must have at most 2 integral digits and 5 " - + "fractional digits"))); + String description = this.resolver.resolveDescription( + new Constraint(Digits.class.getName(), configuration)); + assertThat(description, is(equalTo( + "Must have at most 2 integral digits and 5 " + "fractional digits"))); } @Test public void defaultMessageFuture() { - String description = this.resolver.resolveDescription(new Constraint(Future.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Future.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the future"))); } @@ -106,8 +106,8 @@ public void defaultMessageFuture() { public void defaultMessageMax() { Map configuration = new HashMap<>(); configuration.put("value", 10); - String description = this.resolver.resolveDescription(new Constraint(Max.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Max.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at most 10"))); } @@ -115,8 +115,8 @@ public void defaultMessageMax() { public void defaultMessageMin() { Map configuration = new HashMap<>(); configuration.put("value", 10); - String description = this.resolver.resolveDescription(new Constraint(Min.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Min.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at least 10"))); } @@ -129,15 +129,15 @@ public void defaultMessageNotNull() { @Test public void defaultMessageNull() { - String description = this.resolver.resolveDescription(new Constraint(Null.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Null.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be null"))); } @Test public void defaultMessagePast() { - String description = this.resolver.resolveDescription(new Constraint(Past.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Past.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the past"))); } @@ -145,8 +145,8 @@ public void defaultMessagePast() { public void defaultMessagePattern() { Map configuration = new HashMap<>(); configuration.put("regexp", "[A-Z][a-z]+"); - String description = this.resolver.resolveDescription(new Constraint( - Pattern.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(Pattern.class.getName(), configuration)); assertThat(description, is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); } @@ -156,8 +156,8 @@ public void defaultMessageSize() { Map configuration = new HashMap<>(); configuration.put("min", 2); configuration.put("max", 10); - String description = this.resolver.resolveDescription(new Constraint(Size.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Size.class.getName(), configuration)); assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive"))); } @@ -167,9 +167,10 @@ public void customMessage() { @Override public URL getResource(String name) { - if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) { - return super - .getResource("org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); + if (name.startsWith( + "org/springframework/restdocs/constraints/ConstraintDescriptions")) { + return super.getResource( + "org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); } return super.getResource(name); } @@ -194,14 +195,14 @@ public void customResourceBundle() { @Override protected Object[][] getContents() { - return new String[][] { { NotNull.class.getName() + ".description", - "Not null" } }; + return new String[][] { + { NotNull.class.getName() + ".description", "Not null" } }; } }; String description = new ResourceBundleConstraintDescriptionResolver(bundle) - .resolveDescription(new Constraint(NotNull.class.getName(), Collections - .emptyMap())); + .resolveDescription(new Constraint(NotNull.class.getName(), + Collections.emptyMap())); assertThat(description, is(equalTo("Not null"))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 89bce77b8..4ff83c65c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -66,10 +66,8 @@ public void multipleFieldConstraints() { List constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class); assertThat(constraints, hasSize(2)); - assertThat( - constraints, - containsInAnyOrder(constraint(NotNull.class), constraint(Size.class) - .config("min", 8).config("max", 16))); + assertThat(constraints, containsInAnyOrder(constraint(NotNull.class), + constraint(Size.class).config("min", 8).config("max", 16))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 21c1973d6..c614cb838 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -21,6 +21,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -59,101 +60,98 @@ public class CurlRequestSnippetTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .build()); + new CurlRequestSnippet().document( + new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder("non-get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").build()); + new CurlRequestSnippet().document( + new OperationBuilder("non-get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest("request-with-content").withContents( - codeBlock("bash") + this.snippet.expectCurlRequest("request-with-content") + .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); new CurlRequestSnippet().document(new OperationBuilder("request-with-content", this.snippet.getOutputDirectory()).request("http://localhost/foo") - .content("content").build()); + .content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(new OperationBuilder( - "request-with-query-string", this.snippet.getOutputDirectory()).request( - "http://localhost/foo?param=value").build()); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet() + .document(new OperationBuilder("request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string", this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param=value").method("POST").build()); + this.snippet.expectCurlRequest("post-request-with-query-string") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value") + .method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + this.snippet.expectCurlRequest("post-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() .document(new OperationBuilder("post-request-with-one-parameter", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("post-request-with-multiple-parameters") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-multiple-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-multiple-parameters", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "v1", "v1-bis") + .param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet - .expectCurlRequest("post-request-with-url-encoded-parameter") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + this.snippet.expectCurlRequest("post-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-url-encoded-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet - .expectCurlRequest("post-request-with-query-string-and-parameter") - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string-and-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + this.snippet.expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-query-string-and-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); } @Test @@ -161,62 +159,60 @@ public void postRequestWithOverlappingQueryStringAndParameters() throws IOExcept this.snippet .expectCurlRequest( "post-request-with-overlapping-query-string-and-parameters") - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-overlapping-query-string-and-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + "post-request-with-overlapping-query-string-and-parameters", + this.snippet.getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-one-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + this.snippet.expectCurlRequest("put-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + new CurlRequestSnippet() + .document(new OperationBuilder("put-request-with-one-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("put-request-with-multiple-parameters") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); new CurlRequestSnippet() .document(new OperationBuilder("put-request-with-multiple-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "v1").param("k1", "v1-bis") + .param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("PUT").param("k1", "a&b").build()); + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + new CurlRequestSnippet() + .document(new OperationBuilder("put-request-with-url-encoded-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest("request-with-headers").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i" - + " -H 'Content-Type: application/json' -H 'a: alpha'")); + this.snippet.expectCurlRequest("request-with-headers") + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); new CurlRequestSnippet().document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + this.snippet.getOutputDirectory()) + .request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test @@ -226,11 +222,14 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-no-original-filename", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-no-original-filename", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); } @Test @@ -238,15 +237,17 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + this.snippet.expectCurlRequest("multipart-post-with-content-type") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-with-content-type", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -254,14 +255,15 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest("multipart-post").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + this.snippet.expectCurlRequest("multipart-post") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet().document( + new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -270,45 +272,46 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + this.snippet.expectCurlRequest("multipart-post-with-parameters") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-with-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes").withContents( - containsString("curl request title")); + this.snippet.expectCurlRequest("custom-attributes") + .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); new CurlRequestSnippet(attributes(key("title").value("curl request title"))) - .document(new OperationBuilder("custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + .document(new OperationBuilder("custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest("basic-auth").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet().document(new OperationBuilder("basic-auth", this.snippet - .getOutputDirectory()) - .request("http://localhost/foo") - .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) - .build()); + this.snippet.expectCurlRequest("basic-auth").withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); + new CurlRequestSnippet().document( + new OperationBuilder("basic-auth", this.snippet.getOutputDirectory()) + .request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils + .encodeToString("user:secret".getBytes())) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java index bfdc70107..634fede11 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.restdocs.operation.Parameters; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,23 +44,23 @@ public class QueryStringParserTests { @Test public void noParameters() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost")); assertThat(parameters.size(), is(equalTo(0))); } @Test public void singleParameter() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=alpha")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=alpha")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); } @Test public void multipleParameters() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=alpha&b=bravo&c=charlie")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=alpha&b=bravo&c=charlie")); assertThat(parameters.size(), is(equalTo(3))); assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); @@ -68,16 +69,16 @@ public void multipleParameters() { @Test public void multipleParametersWithSameKey() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=apple&a=avocado")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=apple&a=avocado")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); } @Test public void encoded() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=al%26%3Dpha")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=al%26%3Dpha")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index b5b2d1e4c..279371693 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -56,44 +57,51 @@ public class RequestHeadersSnippetTests { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers").withContents( - tableWithHeader("Name", "Description").row("X-Test", "one") + this.snippet.expectRequestHeaders("request-with-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") .row("Accept", "two").row("Accept-Encoding", "three") .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); - new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), headerWithName("Accept") - .description("two"), headerWithName("Accept-Encoding") - .description("three"), headerWithName("Accept-Language") - .description("four"), headerWithName("Cache-Control") - .description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four"), + headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six"))) + .document(new OperationBuilder("request-with-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); } @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet - .expectRequestHeaders("case-insensitive-request-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-request-headers", this.snippet.getOutputDirectory()) - .request("/").header("X-test", "test").build()); + this.snippet.expectRequestHeaders("case-insensitive-request-headers") + .withContents( + tableWithHeader("Name", "Description").row("X-Test", "one")); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("case-insensitive-request-headers", + this.snippet.getOutputDirectory()).request("/") + .header("X-test", "test").build()); } @Test public void undocumentedRequestHeader() throws IOException { - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-request-header", - this.snippet.getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("undocumented-request-header", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .build()); } @Test @@ -102,9 +110,11 @@ public void missingRequestHeader() throws IOException { this.thrown .expectMessage(equalTo("Headers with the following names were not found" + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()).request("http://localhost").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Accept").description("one"))) + .document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost").build()); } @Test @@ -113,11 +123,13 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio this.thrown .expectMessage(endsWith("Headers with the following names were not found" + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Accept").description("one"))) + .document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").build()); } @Test @@ -129,21 +141,26 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .row("Accept-Encoding", "two", "bravo") .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-extra-column")); + given(resolver.resolveTemplateResource("request-headers")) + .willReturn(snippetResource("request-headers-with-extra-column")); new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one").attributes( - key("foo").value("alpha")), - headerWithName("Accept-Encoding").description("two").attributes( - key("foo").value("bravo")), - headerWithName("Accept").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") - .header("Accept", "*/*").build()); + headerWithName("X-Test").description("one") + .attributes(key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Accept").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .header("X-Test", "test") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept", "*/*").build()); } @Test @@ -151,20 +168,22 @@ public void requestHeadersWithCustomAttributes() throws IOException { this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-title")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test").build()); + given(resolver.resolveTemplateResource("request-headers")) + .willReturn(snippetResource("request-headers-with-title")); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .header("X-Test", "test").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 24487094f..8f5dd609f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -56,58 +57,65 @@ public class ResponseHeadersSnippetTests { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents( - tableWithHeader("Name", "Description").row("X-Test", "one") + this.snippet.expectResponseHeaders("response-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") .row("Content-Type", "two").row("Etag", "three") .row("Cache-Control", "five").row("Vary", "six")); - new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), - headerWithName("Content-Type").description("two"), headerWithName("Etag") - .description("three"), headerWithName("Cache-Control") - .description("five"), headerWithName("Vary").description("six"))) - .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0") - .header("Vary", "User-Agent").build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), + headerWithName("Etag").description("three"), + headerWithName("Cache-Control").description("five"), + headerWithName("Vary").description("six"))) + .document(new OperationBuilder("response-headers", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); } @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet - .expectResponseHeaders("case-insensitive-response-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-response-headers", this.snippet.getOutputDirectory()) - .response().header("X-test", "test").build()); + this.snippet.expectResponseHeaders("case-insensitive-response-headers") + .withContents( + tableWithHeader("Name", "Description").row("X-Test", "one")); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))).document( + new OperationBuilder("case-insensitive-response-headers", + this.snippet.getOutputDirectory()).response() + .header("X-test", "test").build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents( - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Content-Type", "two", "bravo") - .row("Etag", "three", "charlie")); + .withContents(tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") + .row("Etag", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-extra-column")); + given(resolver.resolveTemplateResource("response-headers")) + .willReturn(snippetResource("response-headers-with-extra-column")); new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one").attributes( - key("foo").value("alpha")), - headerWithName("Content-Type").description("two").attributes( - key("foo").value("bravo")), - headerWithName("Etag").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23").build()); + headerWithName("X-Test").description("one") + .attributes(key("foo").value("alpha")), + headerWithName("Content-Type").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Etag").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .build()); } @Test @@ -115,23 +123,26 @@ public void responseHeadersWithCustomAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-title")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").build()); + given(resolver.resolveTemplateResource("response-headers")) + .willReturn(snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .response().header("X-Test", "test").build()); } @Test public void undocumentedResponseHeader() throws IOException { - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "*/*").build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test") + .header("Content-Type", "*/*").build()); } @Test @@ -140,10 +151,10 @@ public void missingResponseHeader() throws IOException { this.thrown .expectMessage(equalTo("Headers with the following names were not found" + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "missing-response-headers", this.snippet.getOutputDirectory()).response() - .build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(new OperationBuilder("missing-response-headers", + this.snippet.getOutputDirectory()).response().build()); } @Test @@ -152,16 +163,17 @@ public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOExcept this.thrown .expectMessage(endsWith("Headers with the following names were not found" + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index f948fb103..a2975674a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -53,181 +54,200 @@ public class HttpRequestSnippetTests { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( - HttpHeaders.HOST, "localhost")); + this.snippet.expectHttpRequest("get-request") + .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") + .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header("Alpha", "a").build()); + new HttpRequestSnippet().document( + new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").header("Alpha", "a").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string").withContents( - httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, - "localhost")); + this.snippet.expectHttpRequest("get-request-with-query-string") + .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") + .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder( - "get-request-with-query-string", this.snippet.getOutputDirectory()) - .request("http://localhost/foo?bar=baz").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("get-request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?bar=baz").build()); } @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-content") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").content(content).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-content", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest("post-request-with-charset").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-charset") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-charset", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-charset", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST") + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-parameter") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("b&r", "baz").param("a", "alpha") + .build()); } @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(RequestMethod.PUT, "/foo") + this.snippet.expectHttpRequest("put-request-with-content") + .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").content(content).build()); + new HttpRequestSnippet().document(new OperationBuilder("put-request-with-content", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").content(content).build()); } @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("put-request-with-parameter").withContents( - httpRequest(RequestMethod.PUT, "/foo") + this.snippet.expectHttpRequest("put-request-with-parameter") + .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("put-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("b&r", "baz").param("a", "alpha") + .build()); } @Test public void multipartPost() throws IOException { - String expectedContent = createPart(String.format("Content-Disposition: " - + "form-data; " + "name=image%n%n<< data >>")); - this.snippet.expectHttpRequest("multipart-post").withContents( - httpRequest(RequestMethod.POST, "/upload") + String expectedContent = createPart(String.format( + "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); + this.snippet + .expectHttpRequest( + "multipart-post") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()).build()); + new HttpRequestSnippet().document( + new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()).build()); } @Test public void multipartPostWithParameters() throws IOException { - String param1Part = createPart(String.format("Content-Disposition: form-data; " - + "name=a%n%napple"), false); - String param2Part = createPart(String.format("Content-Disposition: form-data; " - + "name=a%n%navocado"), false); - String param3Part = createPart(String.format("Content-Disposition: form-data; " - + "name=b%n%nbanana"), false); - String filePart = createPart(String.format("Content-Disposition: form-data; " - + "name=image%n%n<< data >>")); + String param1Part = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n%napple"), + false); + String param2Part = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n%navocado"), + false); + String param3Part = createPart( + String.format("Content-Disposition: form-data; " + "name=b%n%nbanana"), + false); + String filePart = createPart(String + .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( - httpRequest(RequestMethod.POST, "/upload") + this.snippet + .expectHttpRequest( + "multipart-post-with-parameters") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("multipart-post-with-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test public void multipartPostWithContentType() throws IOException { - String expectedContent = createPart(String - .format("Content-Disposition: form-data; name=image%nContent-Type: " + String expectedContent = createPart( + String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( - httpRequest(RequestMethod.POST, "/upload") + this.snippet + .expectHttpRequest( + "multipart-post-with-content-type") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("multipart-post-with-content-type", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .build()); } @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest("get-request-custom-host").withContents( - httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, - "api.example.com")); + this.snippet.expectHttpRequest("get-request-custom-host") + .withContents(httpRequest(RequestMethod.GET, "/foo") + .header(HttpHeaders.HOST, "api.example.com")); new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com").build()); + .header(HttpHeaders.HOST, "api.example.com").build()); } @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( - containsString("Title for the request")); + this.snippet.expectHttpRequest("request-with-snippet-attributes") + .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) .document(new OperationBuilder("request-with-snippet-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 4959e7774..df19b6bc5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -50,38 +51,41 @@ public class HttpResponseSnippetTests { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response").withContents( - httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(new OperationBuilder("basic-response", - this.snippet.getOutputDirectory()).build()); + this.snippet.expectHttpResponse("basic-response") + .withContents(httpResponse(HttpStatus.OK)); + new HttpResponseSnippet().document( + new OperationBuilder("basic-response", this.snippet.getOutputDirectory()) + .build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse("non-ok-response").withContents( - httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", - this.snippet.getOutputDirectory()).response() - .status(HttpStatus.BAD_REQUEST.value()).build()); + this.snippet.expectHttpResponse("non-ok-response") + .withContents(httpResponse(HttpStatus.BAD_REQUEST)); + new HttpResponseSnippet().document( + new OperationBuilder("non-ok-response", this.snippet.getOutputDirectory()) + .response().status(HttpStatus.BAD_REQUEST.value()).build()); } @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse("response-with-headers").withContents( - httpResponse(HttpStatus.OK).header("Content-Type", "application/json") - .header("a", "alpha")); + this.snippet.expectHttpResponse("response-with-headers") + .withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", - this.snippet.getOutputDirectory()).response() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + this.snippet.getOutputDirectory()) + .response() + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(HttpStatus.OK).content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + this.snippet.expectHttpResponse("response-with-content") + .withContents(httpResponse(HttpStatus.OK).content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-content", this.snippet.getOutputDirectory()).response().content(content).build()); } @@ -90,31 +94,31 @@ public void responseWithContent() throws IOException { public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse("response-with-charset").withContents( - httpResponse(HttpStatus.OK) + this.snippet.expectHttpResponse("response-with-charset") + .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( - containsString("Title for the response")); + this.snippet.expectHttpResponse("response-with-snippet-attributes") + .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) .document(new OperationBuilder("response-with-snippet-attributes", - this.snippet.getOutputDirectory()).attribute( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 4ff3dbb4e..9c923e440 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -47,8 +48,8 @@ public class ContentTypeLinkExtractorTests { @Test public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks(this.responseFactory.create( - HttpStatus.OK, new HttpHeaders(), null)); + new ContentTypeLinkExtractor().extractLinks( + this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null)); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 47a05424a..7353bbeae 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -28,6 +28,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; + import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; @@ -100,7 +101,8 @@ public void linksInTheWrongFormat() throws IOException { assertLinks(Collections.emptyList(), links); } - private void assertLinks(List expectedLinks, Map> actualLinks) { + private void assertLinks(List expectedLinks, + Map> actualLinks) { MultiValueMap expectedLinksByRel = new LinkedMultiValueMap<>(); for (Link expectedLink : expectedLinks) { expectedLinksByRel.add(expectedLink.getRel(), expectedLink); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 07adce26d..5f07e6ddf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.SnippetException; @@ -58,22 +59,25 @@ public class LinksSnippetTests { @Test public void undocumentedLink() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [foo]")); + this.thrown.expectMessage(equalTo( + "Links with the following relations were not" + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()).document(new OperationBuilder( - "undocumented-link", this.snippet.getOutputDirectory()).build()); + Collections.emptyList()) + .document(new OperationBuilder("undocumented-link", + this.snippet.getOutputDirectory()).build()); } @Test public void ignoredLink() throws IOException { this.snippet.expectLinks("ignored-link").withContents( tableWithHeader("Relation", "Description").row("b", "Link b")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), - new LinkDescriptor("b").description("Link b"))) - .document(new OperationBuilder("ignored-link", this.snippet - .getOutputDirectory()).build()); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").ignored(), + new LinkDescriptor("b").description("Link b"))) + .document(new OperationBuilder("ignored-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -81,9 +85,10 @@ public void missingLink() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar"))).document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(new OperationBuilder("missing-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -92,17 +97,18 @@ public void documentedOptionalLink() throws IOException { tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", this.snippet - .getOutputDirectory()).build()); + .document(new OperationBuilder("documented-optional-link", + this.snippet.getOutputDirectory()).build()); } @Test public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar").optional())).document(new OperationBuilder( - "missing-optional-link", this.snippet.getOutputDirectory()).build()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) + .document(new OperationBuilder("missing-optional-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -112,64 +118,74 @@ public void undocumentedLinkAndMissingLink() throws IOException { + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( + new OperationBuilder("undocumented-link-and-missing-link", + this.snippet.getOutputDirectory()).build()); } @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks("documented-links").withContents( - tableWithHeader("Relation", "Description").row("a", "one") + this.snippet.expectLinks("documented-links") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") .row("b", "two")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), Arrays.asList( - new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))) - .document(new OperationBuilder("documented-links", this.snippet - .getOutputDirectory()).build()); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two"))) + .document(new OperationBuilder("documented-links", + this.snippet.getOutputDirectory()).build()); } @Test public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( - tableWithHeader("Relation", "Description", "Foo") + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); + this.snippet.expectLinks("links-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), Arrays.asList( - new LinkDescriptor("a").description("one").attributes( - key("foo").value("alpha")), - new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "links-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()).attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList( + new LinkDescriptor("a").description("one") + .attributes(key("foo").value("alpha")), + new LinkDescriptor("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(new OperationBuilder( + "links-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .build()); } @Test public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-title.snippet")); - this.snippet.expectLinks("links-with-custom-attributes").withContents( - startsWith(".Title for the links")); - - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), Arrays.asList( - new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")), attributes(key("title") - .value("Title for the links"))).document(new OperationBuilder( - "links-with-custom-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-title.snippet")); + this.snippet.expectLinks("links-with-custom-attributes") + .withContents(startsWith(".Title for the links")); + + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")), + attributes(key("title").value("Title for the links"))) + .document(new OperationBuilder("links-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } private static class StubLinkExtractor implements LinkExtractor { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 330381f68..4e45a742f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.junit.Test; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java index 622dc5a80..62e69a367 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import org.junit.Test; + import org.springframework.restdocs.operation.OperationRequest; import static org.hamcrest.CoreMatchers.is; @@ -44,14 +45,14 @@ public void delegationOccurs() { OperationRequest preprocessedRequest3 = mock(OperationRequest.class); given(preprocessor1.preprocess(originalRequest)).willReturn(preprocessedRequest1); - given(preprocessor2.preprocess(preprocessedRequest1)).willReturn( - preprocessedRequest2); - given(preprocessor3.preprocess(preprocessedRequest2)).willReturn( - preprocessedRequest3); + given(preprocessor2.preprocess(preprocessedRequest1)) + .willReturn(preprocessedRequest2); + given(preprocessor3.preprocess(preprocessedRequest2)) + .willReturn(preprocessedRequest3); OperationRequest result = new DelegatingOperationRequestPreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalRequest); + .preprocess(originalRequest); assertThat(result, is(preprocessedRequest3)); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java index 0ddb85a31..422393d62 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import org.junit.Test; + import org.springframework.restdocs.operation.OperationResponse; import static org.hamcrest.CoreMatchers.is; @@ -43,16 +44,16 @@ public void delegationOccurs() { OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse3 = mock(OperationResponse.class); - given(preprocessor1.preprocess(originalResponse)).willReturn( - preprocessedResponse1); - given(preprocessor2.preprocess(preprocessedResponse1)).willReturn( - preprocessedResponse2); - given(preprocessor3.preprocess(preprocessedResponse2)).willReturn( - preprocessedResponse3); + given(preprocessor1.preprocess(originalResponse)) + .willReturn(preprocessedResponse1); + given(preprocessor2.preprocess(preprocessedResponse1)) + .willReturn(preprocessedResponse2); + given(preprocessor3.preprocess(preprocessedResponse2)) + .willReturn(preprocessedResponse3); OperationResponse result = new DelegatingOperationResponsePreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalResponse); + .preprocess(originalResponse); assertThat(result, is(preprocessedResponse3)); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 634ef6162..bd3b07792 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import org.junit.Test; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index 2dfdf64b2..a8d999fc4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -22,13 +22,13 @@ import java.util.List; import java.util.Map; -import org.junit.Test; -import org.springframework.restdocs.hypermedia.Link; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Test; + +import org.springframework.restdocs.hypermedia.Link; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -59,8 +59,9 @@ public void halLinksAreMasked() throws Exception { @Test public void formattedHalLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent( - formattedHalPayloadWithLinks(this.links), null), + assertThat( + this.contentModifier + .modifyContent(formattedHalPayloadWithLinks(this.links), null), is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); } @@ -72,16 +73,17 @@ public void atomLinksAreMasked() throws Exception { @Test public void formattedAtomLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent( - formattedAtomPayloadWithLinks(this.links), null), + assertThat( + this.contentModifier + .modifyContent(formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); } @Test public void maskCanBeCustomized() throws Exception { assertThat( - new LinkMaskingContentModifier("custom").modifyContent( - formattedAtomPayloadWithLinks(this.links), null), + new LinkMaskingContentModifier("custom") + .modifyContent(formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 9d37ed78e..f8ea398d3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -20,6 +20,7 @@ import java.util.regex.Pattern; import org.junit.Test; + import org.springframework.http.MediaType; import static org.hamcrest.CoreMatchers.equalTo; @@ -65,8 +66,9 @@ public void encodingIsPreserved() { Pattern pattern = Pattern.compile("[0-9]+"); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( pattern, "<>"); - assertThat(contentModifier.modifyContent((japaneseContent + " 123").getBytes(), - new MediaType("text", "plain", Charset.forName("UTF-8"))), + assertThat( + contentModifier.modifyContent((japaneseContent + " 123").getBytes(), + new MediaType("text", "plain", Charset.forName("UTF-8"))), is(equalTo((japaneseContent + " <>").getBytes()))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index a06c94006..8e5633a75 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -18,6 +18,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; @@ -37,25 +38,25 @@ public class PrettyPrintingContentModifierTests { @Test public void prettyPrintJson() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent( - "{\"a\":5}".getBytes(), null), equalTo(String.format("{%n \"a\" : 5%n}") - .getBytes())); + assertThat(new PrettyPrintingContentModifier() + .modifyContent("{\"a\":5}".getBytes(), null), + equalTo(String.format("{%n \"a\" : 5%n}").getBytes())); } @Test public void prettyPrintXml() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent( - "".getBytes(), null), - equalTo(String.format( - "%n" + assertThat( + new PrettyPrintingContentModifier().modifyContent( + "".getBytes(), null), + equalTo(String + .format("%n" + "%n %n%n") - .getBytes())); + .getBytes())); } @Test public void empytContentIsHandledGracefully() throws Exception { - assertThat( - new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), + assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), equalTo("".getBytes())); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 1f9415979..01af20974 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -22,9 +22,8 @@ import java.util.List; import java.util.Map; -import org.junit.Test; - import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; @@ -95,8 +94,8 @@ public void extractNestedArray() { Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload), equalTo((Object) Arrays.asList(entry1, entry2, entry3))); @@ -108,8 +107,8 @@ public void extractFromItemsInNestedArray() { Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); assertThat( this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload), @@ -122,12 +121,13 @@ public void extractArraysFromItemsInNestedArray() { Map entry1 = createEntry("ids", Arrays.asList(1, 2)); Map entry2 = createEntry("ids", Arrays.asList(3)); Map entry3 = createEntry("ids", Arrays.asList(4)); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), - payload), equalTo((Object) Arrays.asList(Arrays.asList(1, 2), - Arrays.asList(3), Arrays.asList(4)))); + assertThat( + this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload), + equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), + Arrays.asList(4)))); } @Test(expected = FieldDoesNotExistException.class) @@ -202,8 +202,8 @@ public void removeNestedMapEntry() { @SuppressWarnings("unchecked") @Test public void removeItemsInArray() throws IOException { - Map payload = new ObjectMapper().readValue( - "{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload); assertThat(payload.size(), equalTo(0)); } @@ -211,8 +211,8 @@ public void removeItemsInArray() throws IOException { @SuppressWarnings("unchecked") @Test public void removeItemsInNestedArray() throws IOException { - Map payload = new ObjectMapper().readValue( - "{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); + Map payload = new ObjectMapper() + .readValue("{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload); assertThat(payload.size(), equalTo(0)); } @@ -223,8 +223,8 @@ public void extractNestedEntryWithDotInKeys() throws IOException { Map alpha = new HashMap<>(); payload.put("a.key", alpha); alpha.put("b.key", "bravo"); - assertThat(this.fieldProcessor.extract( - JsonFieldPath.compile("['a.key']['b.key']"), payload), + assertThat(this.fieldProcessor + .extract(JsonFieldPath.compile("['a.key']['b.key']"), payload), equalTo((Object) "bravo")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java index 7ef74aa7f..502bfd29c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java @@ -19,12 +19,11 @@ import java.io.IOException; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import com.fasterxml.jackson.databind.ObjectMapper; - import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; @@ -73,30 +72,33 @@ public void stringField() throws IOException { @Test public void nestedField() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c", - createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a.b.c", + createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT)); } @Test public void multipleFieldsWithSameType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), equalTo(JsonFieldType.NUMBER)); } @Test public void multipleFieldsWithDifferentTypes() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), equalTo(JsonFieldType.VARIES)); } @Test public void nonExistentFieldProducesIllegalArgumentException() throws IOException { this.thrownException.expect(FieldDoesNotExistException.class); - this.thrownException - .expectMessage("The payload does not contain a field with the path 'a.b'"); + this.thrownException.expectMessage( + "The payload does not contain a field with the path 'a.b'"); this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 0fedbdee4..40097f3df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -58,69 +59,74 @@ public class RequestFieldsSnippetTests { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + this.snippet.expectRequestFields("map-request-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") .row("a.b", "Number", "one").row("a.c", "String", "two") .row("a", "Object", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))).document(new OperationBuilder( - "map-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + fieldWithPath("a").description("three"))) + .document(new OperationBuilder("map-request-with-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + this.snippet.expectRequestFields("array-request-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") .row("[]a", "Object", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), - fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a") - .description("three"))).document(new OperationBuilder( - "array-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))) + .document(new OperationBuilder("array-request-with-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") + .build()); } @Test public void undocumentedRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5}").build()); } @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields("ignored-request-field").withContents( - tableWithHeader("Path", "Type", "Description").row("b", "Number", - "Field b")); + this.snippet.expectRequestFields("ignored-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); + .document(new OperationBuilder("ignored-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void missingRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a.b]")); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields", this.snippet - .getOutputDirectory()).request("http://localhost").content("{}") - .build()); + .document(new OperationBuilder("missing-request-fields", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{}").build()); } @Test @@ -128,17 +134,16 @@ public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", this.snippet - .getOutputDirectory()).request("http://localhost").content("{ }") - .build()); + "missing-optional-request-field-with-no-type", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{ }").build()); } @Test public void undocumentedRequestFieldAndMissingRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a.b]")); @@ -146,153 +151,163 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .document(new OperationBuilder( "undocumented-request-field-and-missing-request-field", this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + .content("{ \"a\": { \"c\": 5 }}").build()); } @Test public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-extra-column")); - this.snippet.expectRequestFields( - "request-fields-with-custom-descriptor-attributes").withContents( - tableWithHeader("Path", "Type", "Description", "Foo") + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-extra-column")); + this.snippet + .expectRequestFields("request-fields-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") .row("a", "Object", "three", "charlie")); new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one").attributes( - key("foo").value("alpha")), - fieldWithPath("a.c").description("two").attributes( - key("foo").value("bravo")), - fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-fields-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + fieldWithPath("a.b").description("one") + .attributes(key("foo").value("alpha")), + fieldWithPath("a.c").description("two") + .attributes(key("foo").value("bravo")), + fieldWithPath("a").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "request-fields-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-title")); + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-title")); this.snippet.expectRequestFields("request-fields-with-custom-attributes") .withContents(startsWith(".Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("request-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-list-description")); + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-list-description")); this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader("Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( - Arrays.asList("one", "two")))) - .document(new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); + .withContents(tableWithHeader("Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); + + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("a").description(Arrays.asList("one", "two")))).document( + new OperationBuilder("request-fields-with-list-description", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields("xml-request").withContents( - tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") - .row("a/c", "c", "two").row("a", "a", "three")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") - .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectRequestFields("xml-request") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a/b", "b", "one").row("a/c", "c", "two") + .row("a", "a", "three")); + + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), + fieldWithPath("a").description("three") + .type("a"))).document(new OperationBuilder("xml-request", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .document(new OperationBuilder("missing-xml-request", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-request-fields", this.snippet.getOutputDirectory()) - .request("http://localhost").content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))) + .document( + new OperationBuilder("missing-xml-request-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException { + public void undocumentedXmlRequestFieldAndMissingXmlRequestField() + throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) .document(new OperationBuilder( "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ad6653d38..a33a4fec4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -58,261 +59,282 @@ public class ResponseFieldsSnippetTests { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields("map-response-with-fields").withContents( - tableWithHeader("Path", "Type", "Description").row("id", "Number", "one") - .row("date", "String", "two").row("assets", "Array", "three") - .row("assets[]", "Object", "four") + this.snippet.expectResponseFields("map-response-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("id", "Number", "one").row("date", "String", "two") + .row("assets", "Array", "three").row("assets[]", "Object", "four") .row("assets[].id", "Number", "five") .row("assets[].name", "String", "six")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), - fieldWithPath("date").description("two"), fieldWithPath("assets") - .description("three"), + fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(new OperationBuilder("map-response-with-fields", this.snippet - .getOutputDirectory()) - .response() - .content( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); + .document(new OperationBuilder("map-response-with-fields", + this.snippet.getOutputDirectory()) + .response() + .content( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); } @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + this.snippet.expectResponseFields("array-response-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") .row("[]a", "Object", "three")); - new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") - .description("two"), fieldWithPath("[]a").description("three"))) - .document(new OperationBuilder("array-response-with-fields", this.snippet - .getOutputDirectory()).response() - .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))) + .document(new OperationBuilder("array-response-with-fields", + this.snippet.getOutputDirectory()) + .response() + .content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") + .build()); } @Test public void arrayResponse() throws IOException { this.snippet.expectResponseFields("array-response") - .withContents( - tableWithHeader("Path", "Type", "Description").row("[]", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("[]", + "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(new OperationBuilder("array-response", this.snippet - .getOutputDirectory()).response() - .content("[\"a\", \"b\", \"c\"]").build()); + .document(new OperationBuilder("array-response", + this.snippet.getOutputDirectory()).response() + .content("[\"a\", \"b\", \"c\"]").build()); } @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields("ignored-response-field").withContents( - tableWithHeader("Path", "Type", "Description").row("b", "Number", - "Field b")); + this.snippet.expectResponseFields("ignored-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-response-field", this.snippet - .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") - .build()); + .document(new OperationBuilder("ignored-response-field", + this.snippet.getOutputDirectory()).response() + .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-extra-column")); + given(resolver.resolveTemplateResource("response-fields")) + .willReturn(snippetResource("response-fields-with-extra-column")); this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents( - tableWithHeader("Path", "Type", "Description", "Foo") - .row("a.b", "Number", "one", "alpha") - .row("a.c", "String", "two", "bravo") - .row("a", "Object", "three", "charlie")); + .withContents(tableWithHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one").attributes( - key("foo").value("alpha")), - fieldWithPath("a.c").description("two").attributes( - key("foo").value("bravo")), - fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-fields-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + fieldWithPath("a.b").description("one") + .attributes(key("foo").value("alpha")), + fieldWithPath("a.c").description("two") + .attributes(key("foo").value("bravo")), + fieldWithPath("a").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "response-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-title")); + given(resolver.resolveTemplateResource("response-fields")) + .willReturn(snippetResource("response-fields-with-title")); this.snippet.expectResponseFields("response-fields-with-custom-attributes") .withContents(startsWith(".Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": \"foo\"}").build()); + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("response-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .response().content("{\"a\": \"foo\"}").build()); } @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields("xml-response").withContents( - tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") - .row("a/c", "c", "two").row("a", "a", "three")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") - .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-response", this.snippet - .getOutputDirectory()) - .response() - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("xml-response") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a/b", "b", "one").row("a/c", "c", "two") + .row("a", "a", "three")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), + fieldWithPath("a").description("three") + .type("a"))).document(new OperationBuilder("xml-response", + this.snippet.getOutputDirectory()).response() + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new ResponseFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new ResponseFieldsSnippet( + Collections.emptyList()) + .document( + new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()).response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields("xml-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( - "a/@id", "c", "two")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("xml-attribute") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "b", "one").row("a/@id", "c", "two")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlAttribute() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/@id]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( - "a/@id", "c", "two")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c") - .optional())).document(new OperationBuilder( - "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) - .response().content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("missing-optional-xml-attribute") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "b", "one").row("a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c").optional())) + .document( + new OperationBuilder("missing-optional-xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("a"))).document(new OperationBuilder("undocumented-attribute", - this.snippet.getOutputDirectory()).response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("a"))) + .document( + new OperationBuilder("undocumented-attribute", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void documentedXmlAttributesAreRemoved() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo(String - .format("The following parts of the payload were not documented:" + this.thrown.expectMessage(equalTo( + String.format("The following parts of the payload were not documented:" + "%nbar%n"))); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", this.snippet.getOutputDirectory()) - .response().content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList( + fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", this.snippet - .getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .document(new OperationBuilder("xml-response-no-field-type", + this.snippet.getOutputDirectory()).response().content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/b]")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-response-field", this.snippet.getOutputDirectory()) - .response().content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))) + .document( + new OperationBuilder("missing-xml-response-field", + this.snippet.getOutputDirectory()).response() + .content("") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlResponseFieldAndMissingXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"))) + .document( + new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()).response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 4f2ee0b23..885ba390d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -62,8 +63,10 @@ public void undocumentedPathParameter() throws IOException { + " not documented: [a]")); new PathParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/") + .build()); } @Test @@ -73,9 +76,10 @@ public void missingPathParameter() throws IOException { + " not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); + .document(new OperationBuilder("missing-path-parameter", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/") + .build()); } @Test @@ -85,23 +89,26 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " not documented: [b]. Path parameters with the following" + " names were not found in the request: [a]")); new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder( - "undocumented-and-missing-path-parameters", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + Arrays.asList(parameterWithName("a").description("one"))).document( + new OperationBuilder("undocumented-and-missing-path-parameters", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{b}") + .build()); } @Test public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("a", - "one").row("b", "two")); - new PathParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + .row("a", "one").row("b", "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))).document(new OperationBuilder( + "path-parameters", this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -110,10 +117,11 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + parameterWithName("b").description("two"))).document(new OperationBuilder( + "ignored-path-parameter", this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -122,59 +130,69 @@ public void pathParametersWithQueryString() throws IOException { .withContents( tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") .row("a", "one").row("b", "two")); - new PathParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))) - .document(new OperationBuilder("path-parameters-with-query-string", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar") - .build()); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))).document( + new OperationBuilder("path-parameters-with-query-string", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}?foo=bar") + .build()); } @Test public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters( - "path-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); - - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) - .document(new OperationBuilder( - "path-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + given(resolver.resolveTemplateResource("path-parameters")) + .willReturn(snippetResource("path-parameters-with-extra-column")); + this.snippet + .expectPathParameters("path-parameters-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); + + new PathParametersSnippet(Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo")))).document(new OperationBuilder( + "path-parameters-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } @Test public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-title")); + given(resolver.resolveTemplateResource("path-parameters")) + .willReturn(snippetResource("path-parameters-with-title")); this.snippet.expectPathParameters("path-parameters-with-custom-attributes") .withContents(startsWith(".The title")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(new OperationBuilder("path-parameters-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + new PathParametersSnippet( + Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document( + new OperationBuilder("path-parameters-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index da61deecc..548b287ca 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -60,9 +61,9 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); + .document(new OperationBuilder("undocumented-parameter", + this.snippet.getOutputDirectory()).request("http://localhost") + .param("a", "alpha").build()); } @Test @@ -71,9 +72,11 @@ public void missingParameter() throws IOException { this.thrown .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder("missing-parameter", this.snippet - .getOutputDirectory()).request("http://localhost").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("missing-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost").build()); } @Test @@ -83,23 +86,27 @@ public void undocumentedAndMissingParameters() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder( - "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))).document( + new OperationBuilder("undocumented-and-missing-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("b", "bravo") + .build()); } @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters("request-parameters").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "request-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("a", "bravo").param("b", "bravo") - .build()); + this.snippet.expectRequestParameters("request-parameters") + .withContents(tableWithHeader("Parameter", "Description").row("a", "one") + .row("b", "two")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("request-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .param("a", "bravo").param("b", "bravo") + .build()); } @Test @@ -108,57 +115,65 @@ public void ignoredRequestParameter() throws IOException { tableWithHeader("Parameter", "Description").row("b", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-request-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); + .document(new OperationBuilder("ignored-request-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); } @Test public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-extra-column")); + this.snippet + .expectRequestParameters( + "request-parameters-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "request-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .param("a", "alpha").param("b", "bravo").build()); + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(new OperationBuilder( + "request-parameters-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-title")); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-title")); this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") .withContents(startsWith(".The title")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))), attributes(key("title").value( - "The title"))).document(new OperationBuilder( - "request-parameters-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .param("a", "alpha").param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document( + new OperationBuilder("request-parameters-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").param("a", "alpha") + .param("b", "bravo").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index b70e936d4..def767471 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.snippet; import org.junit.Test; + import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -33,22 +34,19 @@ public class RestDocumentationContextPlaceholderResolverTests { @Test public void kebabCaseMethodName() throws Exception { - assertThat( - createResolver("dashSeparatedMethodName").resolvePlaceholder( - "method-name"), equalTo("dash-separated-method-name")); + assertThat(createResolver("dashSeparatedMethodName").resolvePlaceholder( + "method-name"), equalTo("dash-separated-method-name")); } @Test public void snakeCaseMethodName() throws Exception { - assertThat( - createResolver("underscoreSeparatedMethodName").resolvePlaceholder( - "method_name"), equalTo("underscore_separated_method_name")); + assertThat(createResolver("underscoreSeparatedMethodName").resolvePlaceholder( + "method_name"), equalTo("underscore_separated_method_name")); } @Test public void camelCaseMethodName() throws Exception { - assertThat( - createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), + assertThat(createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), equalTo("camelCaseMethodName")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 55ae2218e..e3152d7b0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -19,6 +19,7 @@ import java.io.File; import org.junit.Test; + import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -34,7 +35,8 @@ */ public class StandardWriterResolverTests { - private final PlaceholderResolver placeholderResolver = mock(PlaceholderResolver.class); + private final PlaceholderResolver placeholderResolver = mock( + PlaceholderResolver.class); private final StandardWriterResolver resolver = new StandardWriterResolver( this.placeholderResolver); @@ -48,26 +50,29 @@ public void noConfiguredOutputDirectoryAndRelativeInput() { @Test public void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); - assertThat(this.resolver.resolveFile(absolutePath, "bar.txt", - new RestDocumentationContext(null, null, null)), is(new File( - absolutePath, "bar.txt"))); + assertThat( + this.resolver.resolveFile(absolutePath, "bar.txt", + new RestDocumentationContext(null, null, null)), + is(new File(absolutePath, "bar.txt"))); } @Test public void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); - assertThat(this.resolver.resolveFile("bar", "baz.txt", - new RestDocumentationContext(null, null, outputDir)), is(new File( - outputDir, "bar/baz.txt"))); + assertThat( + this.resolver.resolveFile("bar", "baz.txt", + new RestDocumentationContext(null, null, outputDir)), + is(new File(outputDir, "bar/baz.txt"))); } @Test public void configuredOutputAndAbsoluteInput() { File outputDir = new File("foo").getAbsoluteFile(); String absolutePath = new File("bar").getAbsolutePath(); - assertThat(this.resolver.resolveFile(absolutePath, "baz.txt", - new RestDocumentationContext(null, null, outputDir)), is(new File( - absolutePath, "baz.txt"))); + assertThat( + this.resolver.resolveFile(absolutePath, "baz.txt", + new RestDocumentationContext(null, null, outputDir)), + is(new File(absolutePath, "baz.txt"))); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index f356d2da3..4213da4e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -21,6 +21,7 @@ import java.util.Map; import org.junit.Test; + import org.springframework.restdocs.operation.Operation; import static org.hamcrest.CoreMatchers.equalTo; @@ -55,10 +56,8 @@ public void nullAttributesAreTolerated() { @Test public void snippetName() { - assertThat( - new TestTemplatedSnippet(Collections.emptyMap()) - .getSnippetName(), - is(equalTo("test"))); + assertThat(new TestTemplatedSnippet(Collections.emptyMap()) + .getSnippetName(), is(equalTo("test"))); } private static class TestTemplatedSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index ef807ee20..52e0138b6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -24,6 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.Resource; import static org.hamcrest.CoreMatchers.equalTo; @@ -47,8 +48,8 @@ public class StandardTemplateResourceResolverTests { @Test public void customSnippetResolution() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -66,8 +67,8 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/default-test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/default-test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 63364f6a6..111096b56 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,6 +23,7 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; + import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; @@ -48,8 +49,8 @@ public class ExpectedSnippet implements TestRule { @Override public Statement apply(final Statement base, Description description) { - this.outputDirectory = new File("build/" - + description.getTestClass().getSimpleName()); + this.outputDirectory = new File( + "build/" + description.getTestClass().getSimpleName()); return new ExpectedSnippetStatement(base); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e5be81665..5a71d26c0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -90,9 +90,9 @@ public Operation build() { this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context))); return new StandardOperation(this.name, - (this.requestBuilder == null ? new OperationRequestBuilder( - "http://localhost/").buildRequest() : this.requestBuilder - .buildRequest()), + (this.requestBuilder == null + ? new OperationRequestBuilder("http://localhost/").buildRequest() + : this.requestBuilder.buildRequest()), this.responseBuilder.buildResponse(), this.attributes); } @@ -182,7 +182,8 @@ private OperationRequestPartBuilder(String name, byte[] content) { this.content = content; } - public OperationRequestPartBuilder submittedFileName(String submittedFileName) { + public OperationRequestPartBuilder submittedFileName( + String submittedFileName) { this.submittedFileName = submittedFileName; return this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 97818a52e..98c1ccd10 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -29,6 +29,7 @@ import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; + import org.springframework.http.HttpStatus; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; @@ -71,8 +72,8 @@ public static AsciidoctorCodeBlockMatcher codeBlock(String language) { return new AsciidoctorCodeBlockMatcher(language); } - private static abstract class AbstractSnippetContentMatcher extends - BaseMatcher { + private static abstract class AbstractSnippetContentMatcher + extends BaseMatcher { private List lines = new ArrayList<>(); @@ -149,8 +150,8 @@ public T content(String content) { * * @param The type of the matcher */ - public static abstract class HttpMatcher> extends - AsciidoctorCodeBlockMatcher> { + public static abstract class HttpMatcher> + extends AsciidoctorCodeBlockMatcher> { private int headerOffset = 3; @@ -175,8 +176,8 @@ public T header(String name, long value) { /** * A {@link Matcher} for an HTTP response. */ - public static final class HttpResponseMatcher extends - HttpMatcher { + public static final class HttpResponseMatcher + extends HttpMatcher { private HttpResponseMatcher(HttpStatus status) { this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); @@ -200,17 +201,16 @@ private HttpRequestMatcher(RequestMethod requestMethod, String uri) { /** * A {@link Matcher} for an Asciidoctor table. */ - public static final class AsciidoctorTableMatcher extends - AbstractSnippetContentMatcher { + public static final class AsciidoctorTableMatcher + extends AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String title, String... columns) { if (StringUtils.hasText(title)) { this.addLine("." + title); } this.addLine("|==="); - String header = "|" - + StringUtils - .collectionToDelimitedString(Arrays.asList(columns), "|"); + String header = "|" + StringUtils + .collectionToDelimitedString(Arrays.asList(columns), "|"); this.addLine(header); this.addLine(""); this.addLine("|==="); @@ -258,8 +258,8 @@ private boolean snippetFileExists(Object item) { } private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream( - snippetFile), "UTF-8")); + return FileCopyUtils.copyToString( + new InputStreamReader(new FileInputStream(snippetFile), "UTF-8")); } @Override @@ -269,12 +269,12 @@ public void describeMismatch(Object item, Description description) { } else if (this.expectedContents != null) { try { - this.expectedContents - .describeMismatch(read((File) item), description); + this.expectedContents.describeMismatch(read((File) item), + description); } catch (IOException e) { - description.appendText("The contents of " + item - + " cound not be read"); + description + .appendText("The contents of " + item + " cound not be read"); } } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 9449354a8..3045131d0 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -89,7 +89,8 @@ private List extractParts(MockHttpServletRequest servletRe List parts = new ArrayList<>(); parts.addAll(extractServletRequestParts(servletRequest)); if (servletRequest instanceof MockMultipartHttpServletRequest) { - parts.addAll(extractMultipartRequestParts((MockMultipartHttpServletRequest) servletRequest)); + parts.addAll(extractMultipartRequestParts( + (MockMultipartHttpServletRequest) servletRequest)); } return parts; } @@ -103,24 +104,24 @@ private List extractServletRequestParts( return parts; } - private OperationRequestPart createOperationRequestPart(Part part) throws IOException { + private OperationRequestPart createOperationRequestPart(Part part) + throws IOException { HttpHeaders partHeaders = extractHeaders(part); List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); if (part.getContentType() != null && contentTypeHeader == null) { partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); } - return new OperationRequestPartFactory() - .create(part.getName(), - StringUtils.hasText(part.getSubmittedFileName()) ? part - .getSubmittedFileName() : null, FileCopyUtils - .copyToByteArray(part.getInputStream()), partHeaders); + return new OperationRequestPartFactory().create(part.getName(), + StringUtils.hasText(part.getSubmittedFileName()) + ? part.getSubmittedFileName() : null, + FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); } private List extractMultipartRequestParts( MockMultipartHttpServletRequest multipartRequest) throws IOException { List parts = new ArrayList<>(); - for (Entry> entry : multipartRequest - .getMultiFileMap().entrySet()) { + for (Entry> entry : multipartRequest.getMultiFileMap() + .entrySet()) { for (MultipartFile file : entry.getValue()) { parts.add(createOperationRequestPart(file)); } @@ -134,8 +135,9 @@ private OperationRequestPart createOperationRequestPart(MultipartFile file) if (StringUtils.hasText(file.getContentType())) { partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); } - return new OperationRequestPartFactory().create(file.getName(), StringUtils - .hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null, + return new OperationRequestPartFactory().create(file.getName(), + StringUtils.hasText(file.getOriginalFilename()) + ? file.getOriginalFilename() : null, file.getBytes(), partHeaders); } @@ -170,8 +172,10 @@ private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { } private boolean isNonStandardPort(MockHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTPS); + return (SCHEME_HTTP.equals(request.getScheme()) + && request.getServerPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) + && request.getServerPort() != STANDARD_PORT_HTTPS); } private String getRequestUri(MockHttpServletRequest request) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 20f06771c..6bca090a3 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -108,7 +108,8 @@ public SnippetConfigurer snippets() { * @param templateEngine the template engine to use * @return {@code this} */ - public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine templateEngine) { + public RestDocumentationMockMvcConfigurer templateEngine( + TemplateEngine templateEngine) { this.templateEngineConfigurer.setTemplateEngine(templateEngine); return this; } @@ -120,7 +121,8 @@ public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine template * @param writerResolver The writer resolver to use * @return {@code this} */ - public RestDocumentationMockMvcConfigurer writerResolver(WriterResolver writerResolver) { + public RestDocumentationMockMvcConfigurer writerResolver( + WriterResolver writerResolver) { this.writerResolverConfigurer.setWriterResolver(writerResolver); return this; } @@ -157,9 +159,8 @@ void apply(MockHttpServletRequest request) { if (resolverToUse == null) { resolverToUse = new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver( - (RestDocumentationContext) request - .getAttribute(RestDocumentationContext.class - .getName()))); + (RestDocumentationContext) request.getAttribute( + RestDocumentationContext.class.getName()))); } request.setAttribute(WriterResolver.class.getName(), resolverToUse); } @@ -170,8 +171,8 @@ void setWriterResolver(WriterResolver writerResolver) { } - private static final class ConfigurerApplyingRequestPostProcessor implements - RequestPostProcessor { + private static final class ConfigurerApplyingRequestPostProcessor + implements RequestPostProcessor { private final RestDocumentation restDocumentation; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 2c9dc4529..23f8b7936 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -54,8 +54,8 @@ private RestDocumentationRequestBuilders() { */ public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.get(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -78,8 +78,8 @@ public static MockHttpServletRequestBuilder get(URI uri) { */ public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.post(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -102,8 +102,8 @@ public static MockHttpServletRequestBuilder post(URI uri) { */ public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.put(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -126,8 +126,8 @@ public static MockHttpServletRequestBuilder put(URI uri) { */ public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.patch(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -150,8 +150,8 @@ public static MockHttpServletRequestBuilder patch(URI uri) { */ public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.delete(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -174,8 +174,8 @@ public static MockHttpServletRequestBuilder delete(URI uri) { */ public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.options(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -198,8 +198,8 @@ public static MockHttpServletRequestBuilder options(URI uri) { */ public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.head(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -249,8 +249,8 @@ public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI u public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables) { return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders - .fileUpload(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + .fileUpload(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 425cd2fc0..962f12b47 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -59,14 +59,14 @@ public class RestDocumentationResultHandler implements ResultHandler { RestDocumentationResultHandler(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - this(identifier, requestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); + this(identifier, requestPreprocessor, new IdentityOperationResponsePreprocessor(), + snippets); } RestDocumentationResultHandler(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - responsePreprocessor, snippets); + this(identifier, new IdentityOperationRequestPreprocessor(), responsePreprocessor, + snippets); } RestDocumentationResultHandler(String identifier, @@ -116,15 +116,15 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result - .getRequest().getAttribute( + List combinedSnippets = new ArrayList<>( + (List) result.getRequest().getAttribute( "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; } - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { + private static final class IdentityOperationRequestPreprocessor + implements OperationRequestPreprocessor { @Override public OperationRequest preprocess(OperationRequest request) { @@ -133,8 +133,8 @@ public OperationRequest preprocess(OperationRequest request) { } - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { + private static final class IdentityOperationResponsePreprocessor + implements OperationResponsePreprocessor { @Override public OperationResponse preprocess(OperationResponse response) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index 2723d0166..7ac33bd05 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -30,12 +30,11 @@ * * @author Andy Wilkinson */ -public class SnippetConfigurer extends - AbstractNestedConfigurer { +public class SnippetConfigurer + extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList( - CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse()); + private List defaultSnippets = Arrays.asList(CurlDocumentation.curlRequest(), + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index b7024fd4c..bbf44345f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -23,8 +23,8 @@ * * @author Andy Wilkinson */ -public class UriConfigurer extends - AbstractNestedConfigurer { +public class UriConfigurer + extends AbstractNestedConfigurer { /** * The default scheme for documented URIs. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java index 10c8b9a0c..e587e074f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java @@ -18,4 +18,3 @@ * Core classes for using Spring REST Docs with Spring Test's MockMvc. */ package org.springframework.restdocs.mockmvc; - diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java index 6770d31bb..3ca08471e 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java @@ -23,6 +23,7 @@ import javax.servlet.http.Part; import org.junit.Test; + import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; @@ -53,8 +54,8 @@ public class MockMvcOperationRequestFactoryTests { @Test public void httpRequest() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -71,8 +72,8 @@ public void httpRequestWithCustomPort() throws Exception { @Test public void requestWithContextPath() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get( - "/foo/bar").contextPath("/foo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo/bar").contextPath("/foo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo/bar"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -124,8 +125,8 @@ public void getRequestWithParametersProducesUriWithQueryString() throws Exceptio @Test public void getRequestWithQueryStringPopulatesParameters() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo?a=alpha&b=bravo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo?a=alpha&b=bravo"))); assertThat(request.getParameters().size(), is(2)); @@ -148,9 +149,9 @@ public void postRequestWithParameters() throws Exception { @Test public void mockMultipartFileUpload() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .fileUpload("/foo").file( - new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.fileUpload("/foo") + .file(new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.POST)); assertThat(request.getParts().size(), is(1)); @@ -164,10 +165,9 @@ public void mockMultipartFileUpload() throws Exception { @Test public void mockMultipartFileUploadWithContentType() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .fileUpload("/foo").file( - new MockMultipartFile("file", "original", "image/png", - new byte[] { 1, 2, 3, 4 }))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.fileUpload("/foo").file(new MockMultipartFile( + "file", "original", "image/png", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.POST)); assertThat(request.getParts().size(), is(1)); @@ -186,8 +186,8 @@ public void requestWithPart() throws Exception { given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()).willReturn( - new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()) + .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.txt"); mockRequest.addPart(mockPart); @@ -210,8 +210,8 @@ public void requestWithPartWithContentType() throws Exception { given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()).willReturn( - new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()) + .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.png"); given(mockPart.getContentType()).willReturn("image/png"); @@ -229,8 +229,8 @@ public void requestWithPartWithContentType() throws Exception { private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) throws Exception { - return this.factory.createOperationRequest(builder - .buildRequest(new MockServletContext())); + return this.factory + .createOperationRequest(builder.buildRequest(new MockServletContext())); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 76e3dd677..f37ce499a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -128,12 +129,12 @@ public void curlSnippetWithContent() throws Exception { mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); - assertThat(new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( - "$ curl " + "'http://localhost:8080/' -i -X POST " - + "-H 'Accept: application/json' -d 'content'")))); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents(codeBlock("bash") + .content("$ curl " + "'http://localhost:8080/' -i -X POST " + + "-H 'Accept: application/json' -d 'content'")))); } @Test @@ -141,18 +142,15 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform( - post("/?foo=bar").param("foo", "bar").param("a", "alpha") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andDo(document("curl-snippet-with-query-string")); assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( - "$ curl " - + "'http://localhost:8080/?foo=bar' -i -X POST " - + "-H 'Accept: application/json' -d 'a=alpha'")))); + is(snippet().withContents(codeBlock("bash").content( + "$ curl " + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); } @Test @@ -161,8 +159,7 @@ public void linksSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", + .andExpect(status().isOk()).andDo(document("links", links(linkWithRel("rel").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), @@ -176,9 +173,8 @@ public void pathParametersSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", pathParameters(parameterWithName("foo") - .description("The description")))); + .andExpect(status().isOk()).andDo(document("links", pathParameters( + parameterWithName("foo").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -191,9 +187,8 @@ public void requestParametersSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", requestParameters(parameterWithName("foo") - .description("The description")))); + .andExpect(status().isOk()).andDo(document("links", requestParameters( + parameterWithName("foo").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -205,12 +200,10 @@ public void requestFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform( - get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", - requestFields(fieldWithPath("a").description("The description")))); + mockMvc.perform(get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("links", requestFields( + fieldWithPath("a").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -224,12 +217,10 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document( - "links", - responseFields( - fieldWithPath("a").description("The description"), - fieldWithPath("links").description( - "Links to other resources")))); + .andDo(document("links", + responseFields(fieldWithPath("a") + .description("The description"), + fieldWithPath("links").description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -243,8 +234,8 @@ public void parameterizedOutputDirectory() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("{method-name}")); - assertExpectedSnippetFilesExist(new File( - "build/generated-snippets/parameterized-output-directory"), + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/parameterized-output-directory"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @@ -254,20 +245,20 @@ public void multiStep() throws Exception { .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}-{step}")).build(); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); @@ -280,39 +271,32 @@ public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - mockMvc.perform( - get("/").header("a", "alpha").header("b", "bravo") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) - .andExpect(status().isOk()) - .andDo(document("original-request")) - .andDo(document( - "preprocessed-request", - preprocessRequest( - prettyPrint(), + mockMvc.perform(get("/").header("a", "alpha").header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) + .andExpect(status().isOk()).andDo(document("original-request")) + .andDo(document("preprocessed-request", + preprocessRequest(prettyPrint(), removeHeaders("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH), replacePattern(pattern, "\"<>\"")))); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("a", "alpha") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost") - .header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); + is(snippet().withContents(httpRequest(RequestMethod.GET, "/") + .header("a", "alpha").header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Host", "localhost").header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + is(snippet().withContents(httpRequest(RequestMethod.GET, "/") + .header("b", "bravo").header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test @@ -323,35 +307,29 @@ public void preprocessedResponse() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("original-response")) - .andDo(document( - "preprocessed-response", - preprocessResponse(prettyPrint(), maskLinks(), - removeHeaders("a"), + .andExpect(status().isOk()).andDo(document("original-response")) + .andDo(document("preprocessed-response", + preprocessResponse(prettyPrint(), maskLinks(), removeHeaders("a"), replacePattern(pattern, "\"<>\"")))); String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) - .header("a", "alpha") - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - original.getBytes().length).content(original)))); + is(snippet().withContents(httpResponse(HttpStatus.OK).header("a", "alpha") + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, original.getBytes().length) + .content(original)))); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - prettyPrinted.getBytes().length) - .content(prettyPrinted)))); + is(snippet().withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); } @Test @@ -359,8 +337,8 @@ public void customSnippetTemplate() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - ClassLoader classLoader = new URLClassLoader(new URL[] { new File( - "src/test/resources/custom-snippet-templates").toURI().toURL() }, + ClassLoader classLoader = new URLClassLoader(new URL[] { + new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, getClass().getClassLoader()); ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); @@ -372,15 +350,13 @@ public void customSnippetTemplate() throws Exception { finally { Thread.currentThread().setContextClassLoader(previous); } - assertThat(new File( - "build/generated-snippets/custom-snippet-template/curl-request.adoc"), + assertThat( + new File( + "build/generated-snippets/custom-snippet-template/curl-request.adoc"), is(snippet().withContents(equalTo("Custom curl request")))); - mockMvc.perform(get("/")).andDo( - document( - "index", - curlRequest(attributes(key("title").value( - "Access the index using curl"))))); + mockMvc.perform(get("/")).andDo(document("index", curlRequest( + attributes(key("title").value("Access the index using curl"))))); } @Test @@ -392,12 +368,10 @@ public void customContextPath() throws Exception { get("/custom/").contextPath("/custom").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("custom-context-path")); assertThat( - new File("build/generated-snippets/custom-context-path/curl-request.adoc"), - is(snippet() - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); + new File( + "build/generated-snippets/custom-context-path/curl-request.adoc"), + is(snippet().withContents(codeBlock("bash").content( + "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java index 486bfd8e6..eda5107d9 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; @@ -58,7 +59,7 @@ public void defaultConfiguration() { public void customScheme() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( this.restDocumentation).uris().withScheme("https") - .beforeMockMvcCreated(null, null); + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("https", "localhost", 8080); @@ -68,7 +69,7 @@ public void customScheme() { public void customHost() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( this.restDocumentation).uris().withHost("api.example.com") - .beforeMockMvcCreated(null, null); + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "api.example.com", 8080); @@ -77,8 +78,8 @@ public void customHost() { @Test public void customPort() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).uris().withPort(8081) - .beforeMockMvcCreated(null, null); + this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, + null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "localhost", 8081); @@ -87,8 +88,8 @@ public void customPort() { @Test public void noContentLengthHeaderWhenRequestHasNotContent() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).uris().withPort(8081) - .beforeMockMvcCreated(null, null); + this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, + null); postProcessor.postProcessRequest(this.request); assertThat(this.request.getHeader("Content-Length"), is(nullValue())); } @@ -97,8 +98,8 @@ private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); assertEquals(port, this.request.getServerPort()); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes( - this.request)); + RequestContextHolder + .setRequestAttributes(new ServletRequestAttributes(this.request)); try { URI uri = BasicLinkBuilder.linkToCurrentMapping().toUri(); assertEquals(scheme, uri.getScheme()); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index a5bd3159f..0ed8fee98 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -21,6 +21,7 @@ import javax.servlet.ServletContext; import org.junit.Test; + import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; From cfc413f7ed56613cdd645986c4fd85ff0f8f5a66 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 11:30:28 +0000 Subject: [PATCH 0223/1059] Avoid byte[] to String to byte[] conversion when pretty printing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, PrettyPrintingContentModifier would convert the byte[] content into a String and then back into a byte[]. It did so without consideration for the content’s character set. As a result, it could fail to preserve the correct character encoding. This commit updates PrettyPrintingContentModifier to avoid converting the content into a String and back into a byte[] and to work entirely with byte arrays instead. Removing the intermediate String from the process removes the possibility of the content becoming corrupted. Closes gh-202 --- .../PrettyPrintingContentModifier.java | 23 +++++++++++-------- .../PrettyPrintingContentModifierTests.java | 19 ++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index b722038ec..e53ff431e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -17,8 +17,8 @@ package org.springframework.restdocs.operation.preprocess; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringWriter; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -60,7 +60,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType contentType) { if (originalContent.length > 0) { for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { try { - return prettyPrinter.prettyPrint(originalContent).getBytes(); + return prettyPrinter.prettyPrint(originalContent); } catch (Exception ex) { // Continue @@ -72,24 +72,25 @@ public byte[] modifyContent(byte[] originalContent, MediaType contentType) { private interface PrettyPrinter { - String prettyPrint(byte[] content) throws Exception; + byte[] prettyPrint(byte[] content) throws Exception; } private static final class XmlPrettyPrinter implements PrettyPrinter { @Override - public String prettyPrint(byte[] original) throws Exception { + public byte[] prettyPrint(byte[] original) throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); - StringWriter transformed = new StringWriter(); + ByteArrayOutputStream transformed = new ByteArrayOutputStream(); transformer.setErrorListener(new SilentErrorListener()); transformer.transform(createSaxSource(original), new StreamResult(transformed)); - return transformed.toString(); + + return transformed.toByteArray(); } private SAXSource createSaxSource(byte[] original) @@ -145,11 +146,13 @@ public void fatalError(SAXParseException exception) throws SAXException { private static final class JsonPrettyPrinter implements PrettyPrinter { + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true); + @Override - public String prettyPrint(byte[] original) throws IOException { - ObjectMapper objectMapper = new ObjectMapper() - .configure(SerializationFeature.INDENT_OUTPUT, true); - return objectMapper.writeValueAsString(objectMapper.readTree(original)); + public byte[] prettyPrint(byte[] original) throws IOException { + return this.objectMapper + .writeValueAsBytes(this.objectMapper.readTree(original)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 8e5633a75..84accb52a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,12 +16,17 @@ package org.springframework.restdocs.operation.preprocess; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.isEmptyString; import static org.junit.Assert.assertThat; @@ -68,4 +73,16 @@ public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { null), equalTo(content.getBytes())); } + + @Test + public void encodingIsPreserved() throws Exception { + Map input = new HashMap<>(); + input.put("japanese", "\u30b3\u30f3\u30c6\u30f3\u30c4"); + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map output = objectMapper + .readValue(new PrettyPrintingContentModifier().modifyContent( + objectMapper.writeValueAsBytes(input), null), Map.class); + assertThat(output, is(equalTo(input))); + } } From 4d44401efa0afda9291ddfe536191fd7f0813db7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 12:43:32 +0000 Subject: [PATCH 0224/1059] Add a note in the documentation about use of the JVM's default charset Closes gh-201 --- docs/src/docs/asciidoc/configuration.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ae1b601f8..049539969 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -46,6 +46,11 @@ use `RestDocumentationMockMvcConfigurer` to configure it: include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] ---- +TIP: When Spring REST Docs converts a request or response's content to a String, the +`charset` specified in the `Content-Type` header will be used if it is available. In its +absence, the JVM's default `Charset` will be used. The JVM's default `Charset` can be +configured using the `file.encoding` system property. + [[configuration-default-snippets]] From 40201e2e994d7a3acd2c15f91b96574e6eb6579a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 15:41:09 +0000 Subject: [PATCH 0225/1059] Improve handling of empty parameters in curl and HTTP request snippets Previously, both the curl and HTTP request snippets would ignore a parameter with no value, for example from the query string of the url http://localhost:8080/foo?bar. This commit updates both snippets so that such parameters are included in the generated snippet, including a multi-part request that is uploading form data and a field in the form has no value. Additions have been made to the tests for both snippets. While the request parameters snippet correctly handled parameters with no value, there was no test verifying that this was the case. One has been added in this commit. Closes gh-200 --- .../restdocs/curl/QueryStringParser.java | 20 +++++-- .../restdocs/http/HttpRequestSnippet.java | 12 +++-- .../restdocs/operation/Parameters.java | 28 +++++++--- .../curl/CurlRequestSnippetTests.java | 36 ++++++++++++- .../http/HttpRequestSnippetTests.java | 52 ++++++++++++++++++- .../RequestParametersSnippetTests.java | 14 ++++- .../restdocs/test/OperationBuilder.java | 12 +++-- 7 files changed, 154 insertions(+), 20 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java index 0982adf95..ea5f31481 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,6 +19,8 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; import java.util.Scanner; import org.springframework.restdocs.operation.Parameters; @@ -58,10 +60,18 @@ private Parameters parse(String query) { private void processParameter(String parameter, Parameters parameters) { String[] components = parameter.split("="); - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.get(components[0]); + if (values == null) { + parameters.put(components[0], new LinkedList()); + } + } } else { throw new IllegalArgumentException( diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 38d1e35f6..cd855f7be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -127,10 +127,16 @@ private boolean isPutOrPost(OperationRequest request) { private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); for (Entry> parameter : request.getParameters().entrySet()) { - for (String value : parameter.getValue()) { + if (parameter.getValue().isEmpty()) { writePartBoundary(writer); - writePart(parameter.getKey(), value, null, writer); - writer.println(); + writePart(parameter.getKey(), "", null, writer); + } + else { + for (String value : parameter.getValue()) { + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, writer); + writer.println(); + } } } for (OperationRequestPart part : request.getParts()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index 9660a9816..0235ec800 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -40,17 +40,33 @@ public class Parameters extends LinkedMultiValueMap { public String toQueryString() { StringBuilder sb = new StringBuilder(); for (Map.Entry> entry : entrySet()) { - for (String value : entry.getValue()) { - if (sb.length() > 0) { - sb.append("&"); + if (entry.getValue().isEmpty()) { + append(sb, entry.getKey()); + } + else { + for (String value : entry.getValue()) { + append(sb, entry.getKey(), value); } - sb.append(urlEncodeUTF8(entry.getKey())).append('=') - .append(urlEncodeUTF8(value)); } } return sb.toString(); } + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); + } + + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncodeUTF8(key) + "=" + urlEncodeUTF8(value)); + } + + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(toAppend); + } + private static String urlEncodeUTF8(String s) { try { return URLEncoder.encode(s, "UTF-8"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index c614cb838..519644337 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -95,6 +95,17 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectCurlRequest("request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i")); + new CurlRequestSnippet() + .document(new OperationBuilder("request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param").build()); + } + @Test public void postRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string") @@ -107,6 +118,18 @@ public void postRequestWithQueryString() throws IOException { .method("POST").build()); } + @Test + public void postRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param").method("POST") + .build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter") @@ -118,6 +141,17 @@ public void postRequestWithOneParameter() throws IOException { .method("POST").param("k1", "v1").build()); } + @Test + public void postRequestWithOneParameterWithNoValue() throws IOException { + this.snippet.expectCurlRequest("post-request-with-one-parameter-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-one-parameter-with-no-value", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1").build()); + } + @Test public void postRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("post-request-with-multiple-parameters") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index a2975674a..5175edd8c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -75,6 +75,18 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?bar=baz").build()); } + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpRequest("get-request-with-query-string-with-no-value") + .withContents(httpRequest(RequestMethod.GET, "/foo?bar") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document( + new OperationBuilder("get-request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?bar").build()); + } + @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; @@ -123,6 +135,20 @@ public void postRequestWithParameter() throws IOException { .build()); } + @Test + public void postRequestWithParameterWithNoValue() throws IOException { + this.snippet.expectHttpRequest("post-request-with-parameter") + .withContents(httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "application/x-www-form-urlencoded") + .content("bar=")); + + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("bar").build()); + } + @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; @@ -201,6 +227,30 @@ public void multipartPostWithParameters() throws IOException { .part("image", "<< data >>".getBytes()).build()); } + @Test + public void multipartPostWithParameterWithNoValue() throws IOException { + String paramPart = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n"), false); + String filePart = createPart(String + .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); + String expectedContent = paramPart + filePart; + this.snippet + .expectHttpRequest( + "multipart-post-with-parameter-with-no-value") + .withContents(httpRequest(RequestMethod.POST, "/upload") + .header("Content-Type", + "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); + new HttpRequestSnippet().document( + new OperationBuilder("multipart-post-with-parameter-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a").part("image", "<< data >>".getBytes()) + .build()); + } + @Test public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 548b287ca..c4b05f355 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -109,6 +109,18 @@ public void requestParameters() throws IOException { .build()); } + @Test + public void requestParameterWithNoValue() throws IOException { + this.snippet.expectRequestParameters("request-parameter-with-no-value") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("request-parameter-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("a").build()); + } + @Test public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 5a71d26c0..e483d6c51 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,6 +19,7 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -146,8 +147,13 @@ public OperationRequestBuilder content(byte[] content) { } public OperationRequestBuilder param(String name, String... values) { - for (String value : values) { - this.parameters.add(name, value); + if (values.length > 0) { + for (String value : values) { + this.parameters.add(name, value); + } + } + else { + this.parameters.put(name, Collections.emptyList()); } return this; } From 7e2a3dcd18f974bffa8de340a5fa9916270c0566 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 16:01:59 +0000 Subject: [PATCH 0226/1059] Fix faulty forward merge --- .../curl/CurlRequestSnippetTests.java | 19 ++++++------- .../http/HttpRequestSnippetTests.java | 27 ++++++++----------- .../RequestParametersSnippetTests.java | 5 ++-- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 9447cd3d6..2b1371924 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -93,9 +93,8 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i")); new CurlRequestSnippet() - .document(new OperationBuilder("request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param").build()); + .document(operationBuilder("request-with-query-string-with-no-value") + .request("http://localhost/foo?param").build()); } @Test @@ -114,11 +113,9 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i -X POST")); - new CurlRequestSnippet().document( - new OperationBuilder("post-request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param").method("POST") - .build()); + new CurlRequestSnippet() + .document(operationBuilder("post-request-with-query-string-with-no-value") + .request("http://localhost/foo?param").method("POST").build()); } @Test @@ -138,9 +135,9 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); new CurlRequestSnippet().document( - new OperationBuilder("post-request-with-one-parameter-with-no-value", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1").build()); + operationBuilder("post-request-with-one-parameter-with-no-value") + .request("http://localhost/foo").method("POST").param("k1") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index c2436ec98..0660eac8b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -76,10 +76,9 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document( - new OperationBuilder("get-request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?bar").build()); + new HttpRequestSnippet() + .document(operationBuilder("get-request-with-query-string-with-no-value") + .request("http://localhost/foo?bar").build()); } @Test @@ -132,10 +131,8 @@ public void postRequestWithParameterWithNoValue() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("bar=")); - new HttpRequestSnippet() - .document(new OperationBuilder("post-request-with-parameter", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("bar").build()); + new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") + .request("http://localhost/foo").method("POST").param("bar").build()); } @Test @@ -226,14 +223,12 @@ public void multipartPostWithParameterWithNoValue() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document( - new OperationBuilder("multipart-post-with-parameter-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a").part("image", "<< data >>".getBytes()) - .build()); + new HttpRequestSnippet() + .document(operationBuilder("multipart-post-with-parameter-with-no-value") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a").part("image", "<< data >>".getBytes()).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 95a38a2f3..204806522 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -65,9 +65,8 @@ public void requestParameterWithNoValue() throws IOException { tableWithHeader("Parameter", "Description").row("a", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("request-parameter-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost").param("a").build()); + .document(operationBuilder("request-parameter-with-no-value") + .request("http://localhost").param("a").build()); } @Test From c8ecd255913cfeeba251b0de66afcae46b6ab43b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:12:16 +0000 Subject: [PATCH 0227/1059] Add a sample demonstrating how to use REST Assured Closes gh-198 --- build.gradle | 4 + docs/src/docs/asciidoc/getting-started.adoc | 39 +++-- samples/rest-assured/build.gradle | 58 +++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/rest-assured/gradlew | 164 ++++++++++++++++++ samples/rest-assured/gradlew.bat | 90 ++++++++++ .../rest-assured/src/docs/asciidoc/index.adoc | 26 +++ .../SampleRestAssuredApplication.java | 41 +++++ .../SampleRestAssuredApplicationTests.java | 74 ++++++++ samples/testng/src/docs/asciidoc/index.adoc | 5 +- 11 files changed, 491 insertions(+), 16 deletions(-) create mode 100644 samples/rest-assured/build.gradle create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/rest-assured/gradlew create mode 100644 samples/rest-assured/gradlew.bat create mode 100644 samples/rest-assured/src/docs/asciidoc/index.adoc create mode 100644 samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java create mode 100644 samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java diff --git a/build.gradle b/build.gradle index 0e8acc927..2439c1ccd 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,10 @@ samples { testNg { workingDir "$projectDir/samples/testng" } + + restAssured { + workingDir "$projectDir/samples/rest-assured" + } } task api (type: Javadoc) { diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 415836231..485dec89a 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -8,20 +8,31 @@ This section describes how to get started with Spring REST Docs. [[getting-started-sample-applications]] === Sample applications -If you want to jump straight in, there are two sample applications available. -{samples}/rest-notes-spring-hateoas[One sample] uses -http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and -{samples}/rest-notes-spring-data-rest[the other] uses -http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use -Spring REST Docs to produce a detailed API guide and a getting started walkthrough. - -Each sample contains a file named `api-guide.adoc` that produces an API guide for the -service, and a file named `getting-started-guide.adoc` that produces a getting started -guide that provides an introductory walkthrough. - -The code that produces the generated snippets can be found in `src/test/java`. -`ApiDocumentation.java` produces the snippets for the API guide. -`GettingStartedDocumentation.java` produces the snippets for the getting started guide. +If you want to jump straight in, a number of sample applications are available: + +[cols="3,2,10"] +|=== +| Sample | Build system | Description + +| {samples}/rest-assured[REST Assured] +| Gradle +| Demonstrates the use of Spring REST Docs with REST Assured. + +| {samples}/rest-notes-spring-data-rest[Spring Data REST] +| Maven +| Demonstrates the creation of a getting started guide and an API guide for a service + implemented using Spring Data REST. + +| {samples}/rest-notes-spring-hateoas[Spring HATEOAS] +| Gradle +| Demonstrates the creation of a getting started guide and an API guide for a service + implemented using Spring HATEOAS. + +| {samples}/testng[TestNG] +| Gradle +| Demonstrates the use of Spring REST Docs with TestNG. + +|=== diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle new file mode 100644 index 000000000..9ad942399 --- /dev/null +++ b/samples/rest-assured/build.gradle @@ -0,0 +1,58 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +plugins { + id "org.asciidoctor.convert" version "1.5.3" +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile "org.springframework.restdocs:spring-restdocs-restassured:$springRestdocsVersion" +} + +test { + outputs.dir snippetsDir +} + +asciidoctor { + attributes 'snippets': snippetsDir + inputs.dir snippetsDir + dependsOn test +} + +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2b8cbfe8d --- /dev/null +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 16 12:33:24 BST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/rest-assured/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/rest-assured/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..f89aeb6e1 --- /dev/null +++ b/samples/rest-assured/src/docs/asciidoc/index.adoc @@ -0,0 +1,26 @@ += Spring REST Docs REST Assured Sample +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +Sample application demonstrating how to use Spring REST Docs with REST Assured. + +`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service +that is being tested is running on a random port on `localhost`. The tests make use of a +preprocessor to modify the request so that it appears to have been sent to +`https://api.example.com`. If your service includes URIs in its responses, for example +because it uses hypermedia, similar preprocessing can be applied to the response before +it is documented. + +Three snippets are produced. One showing how to make a request using cURL: + +include::{snippets}/sample/curl-request.adoc[] + +One showing the HTTP request: + +include::{snippets}/sample/http-request.adoc[] + +And one showing the HTTP response: + +include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java new file mode 100644 index 000000000..7df98b5f2 --- /dev/null +++ b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class SampleRestAssuredApplication { + + public static void main(String[] args) { + new SpringApplication(SampleRestAssuredApplication.class).run(args); + } + + @RestController + private static class SampleController { + + @RequestMapping("/") + public String index() { + return "Hello, World"; + } + + } + +} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java new file mode 100644 index 000000000..92ef63746 --- /dev/null +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +@SpringApplicationConfiguration(classes=SampleRestAssuredApplication.class) +@WebIntegrationTest("server.port=0") +@RunWith(SpringJUnit4ClassRunner.class) +public class SampleRestAssuredApplicationTests { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + private RequestSpecification documentationSpec; + + @Value("${local.server.port}") + private int port; + + @Before + public void setUp() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)).build(); + } + + @Test + public void sample() throws Exception { + given(this.documentationSpec) + .accept("text/plain") + .filter(document("sample", + preprocessRequest(modifyUris() + .scheme("https") + .host("api.example.com") + .removePort()))) + .when() + .port(this.port) + .get("/") + .then() + .assertThat().statusCode(is(200)); + } + +} diff --git a/samples/testng/src/docs/asciidoc/index.adoc b/samples/testng/src/docs/asciidoc/index.adoc index ff513a31a..ea8b47e49 100644 --- a/samples/testng/src/docs/asciidoc/index.adoc +++ b/samples/testng/src/docs/asciidoc/index.adoc @@ -4,8 +4,9 @@ Andy Wilkinson; :icons: font :source-highlighter: highlightjs -This sample application demonstrates how to use Spring REST Docs with TestNG. -`SampleTestNgApplication` makes a call to a very simple service and produces three +Sample application demonstrating how to use Spring REST Docs with TestNG. + +`SampleTestNgApplicationTests` makes a call to a very simple service and produces three documentation snippets. One showing how to make a request using cURL: From b224333c977b66c6a22895f47d777bebe69a0f6c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:17:36 +0000 Subject: [PATCH 0228/1059] Upgrade to Gradle 2.11 --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/rest-assured/gradlew | 10 +++------- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/rest-notes-spring-hateoas/gradlew | 10 +++------- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/testng/gradlew | 10 +++------- 10 files changed, 17 insertions(+), 29 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ed0a26aa4..0c9568aee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Sep 23 15:50:30 BST 2015 +#Mon Feb 15 17:15:12 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 zcmZ6SQ;;UWvaQ>;ZQHhO+qV6;ZQHhuY1_7@ZFf&&?(BUYZk+R!8Bz84Mb*k&nUnFL zp(UUQO0u9}FhD?1P(Ut1&XP$8K!*>$cNxWL<+K(;|F2F$l|B}UjE>#kN{Ws1~ z{!e^k{BKO50X{(a&%x+k*cuuL5RelX5Re#9%3udU3R5RliU|llATA1;S-x6cPSD;M z)Uw{w%rWV);W@^h?E&(=B(_B;jR+X^Zg}pR?`ejQx99EnZ2b;s%FBi%E*KgX9MqP2 zhodPz4vj-qYwZ>vRkzcY1Y!JFdyp^OB&NYZm40}qDxaCu%22tHR<&=C*D^mh#v{Gp zE49h`FvJ^T*Yk4#08Os49h141@R^ic;njQtS;d=#jZ{2&wt> zi3(-JTN}}Q+Fp9^Icz(nS5(cD@fti}A}8kwRs$LK*( zz2FkzkG1qCKYaHOoUVdDowzikm3lNF0xMr$`b*NrgGGWwP0Q9*J1sdSGE|kXn)!D{ zD_8Z!l|hy>K(2Pt&_WpCJn~$(M5&C`LNso!Q)difr$?G2 zgz$mfC8Hd7GC=&Zr;uwpsIWE0tzzhO0e4yvPZztpriftP^%PlgGF z0)gaa1>=jXkk;eaSpRI)m1P2xuliU#J79OxdBdRm2 z!=X$0iPk%%lb*!y=(9hlT9Yt8gc1HiG|%nREv(%h;bd)LeJc3A(fjEpXK9x>+=3JD}oba&1bgoYA<+yt-;x-A7R|+Y!$!)KMn(kt($$R zfD7CE6MdlzS5{eJ%41(5&0azO*2fwG8eX)g_Vg>)P|MCE0c*+d`q&C3p?GhFb-O8q zJlw0TeKt|c{jDnBaU=ev-FKzA_syaPb?@djE6;wpontG%YriRW zKHuBV&U;MZx2FDC$?mNWg(Wdy$8Q^EjiI+!sczD7{?kRZ$)_ozCA~~}ahay#n4!Mv zx+XiK`tNJCgk(>$rPwBNRfpvvX~4cpyc(f`pyb>gzgU`8XJw@5@{N!08f?y8fLhyu z_74s`I^3dxp#wt$`(=;r?4Q%ynmA5(_>c)!xZD8ZAKC|Yw8V9#qw*O*TsSbi_#W3w zfni!bkKvKT)xs7m6Yx(2kV@o36x!(fO%WrMAzD9~ANMn;QEtICQ=SLa{6xZ);IC(u zYcS`&R4!qbSXJZPAB-u+6nu$fJ8^GuDcJofCwxvcB4=zf z6=)XWWAuMnsRT|RBGg}|uOnTV#8Nnx60oj12f1?3mIHMzkU!Y$l zv1je@C|WIv`T4GQ+yzfGfS;c~@CQhWDN+X(AQ?`yLEDuS${}=O`jNaMJ84ct!buB8 zkqH$Lp4fm5=;1BaVV0dUxTZ_#n@j_H-(T3x`ol*J6~h9adsY4^3LHJ{<=Nxarck_G zv0W8op+zfg`1*_Z?nW*rB_;6^JIdYd^{y48NbL4`MM^jMRfKAzYV`USUGc^j^hruM zkCS4neQ>l0Y6XGXpy6XCexGXYK7M>B>0`QWVKD&A{0=GwvNa0Ra1$*&QP$MWi%^wg zR89AhXP(qSTKkGEgcl1Oe*18<`FtK6L6(sXl^;6Z6+&-`@n}^#n>`ePxuv2T+GK_LgAu zbie?b5i3$Zxu#otQd;Gsg?g<8=tqa6%Z5?ZbCP{-VKDyR&GRy@d8|-|r=^x|m?pFVsnl-Bbif8)KH`DMcG(_De!Ha3imVrX zZ^jbbk_}JT5<68X_L6KE4|?Cu{n6`R;Qx)|oXb@-IwTN~0|F2T5Jj>)Dd2zjOVju; z#nnXqSzAN*PT`7n-DnFjp^T&3#E@2{l19B>6{~CbL!+?OX=_XBNORZSQ!^6of=cz~ z4+fI@34y6YJ<R2z4KksB-uTl^kHR-K+^^G{$*EIT2r%j1ofgR0U6YC-p{`rB_;8WIKCLxFmkW1 z{F4o5h?%pGbdkgCCwuBRexHZck30T$??32F8BREIFUsvr84c)xL5S6g(Q%$|(%aUe z>IyffQF1M>+71z)b#{{+Op;Cc79|~iVpSbZdCC}?m-84pIvdUBx*0@{*y@&6?swffUHp{#gGvt{4 zK|pw!ght17QXci@BN$CkgF7Cl$scA%clFX4RmD0M-*eC&RlJXm2B&+JSue(-!XE8) zsClnC|AG^=lxU_2Ag+OZFN-#edbaH?KA7pT>;!DQ%cBjbGY3^@cc&=4lF6>?i8J>* zi5j|Ggh6>bckJZrugNEp>UXtk-47^5YF6A|tU@x}CPQVSY zN6o<@?wN&!b=|z$Ec&L}`>&Rth3Yh?Ddi^=8B#l?#`UQ?rHXk1P5Q097o;jgtt!11Ja!YPRM+xr7X@_iS00)8%EL7|tZ*9!XX1 zfGbs64J?QLoq6T09OLF1y|-;27{>!^(81H6%XfEXZAjjy?r~~*yVt|vDn0*!M}Q63 z^^zf-wRpwYo<(rOb?tKY@#S;?3Z-2Pn$x-mFiXtw*B;N5V~<4Z(YF^|EsZApgpII5M?EG zw=;9%TyK1Mb(()DIGObc*K6T&3&3q-9f2;2tPyO4$^L`mudJf-T*q=23l1$*T)8g| zR6?0AE4tWR7za{b%76*_^`?+teK1A+3d<|StFslnGnFX-eD-W8ryJ7aoWkSWM@8{P z$qW?oY^m;WO$wa;O82mpc!+sask@2L8qYW#axp~RLQ|yYI%E&#hJ(R50iZW1JJEyz z&*zS+sABV5J4JEJRBo2&MKUv(>E?Qrstk(#V+VY)a}Xesn@|?j*c2;@jz=Ji2R#r} z;u@D#)unW|!rxlIo_fnB>oF#p_NwSu#B)ll#3y~vuEZMTjHn*cYZ(@(X0h{crO$M_@Qw;{SuWC#wtWV?b<+-8P? znBjaW@~I!ndX+Wy%A&H`OV(?ZZxSbA<(=9^@b*Rd-J(mN<23q&1SlI*=et_WDIgAF z%-J%ji2FDvxIsS6?cNZM;J@O>FFKo+(&Q(wFf&p!mMCQ@w-`$y`ms^pYVU0~CYWEA z#EK~N)>HQO9~n>!+vf`Uo8hg%!jdYDBs`BlveZyy%%5`pqA@G-CbrLSM(cIgol1V- zcf@v)hg@H<`TK(uQOqV$K(<^!vSD2-#589NG`Juh1ds4B>q1h{rZ`liR`;TM|JzdXTs*V7%idZ`D_pFDwh)u?*8 z2d0N$j$M{nCzdNy)gr^3eR|hDvr+p64HZ1PwiK*OOUee$9ltEuLEVS-Dj%)^)tx*9 z?2*mSE~$Zg!8)qHlZ0)XEwuyHo!tQ29LAvUAs1>KZVzR=f2e$P%4O4p0bYRGoO_0f zfb7pa1S7uf-n&G9-_RW}aERATfA^~9BO_B(JJAW@Hd#= zQHOa_@1PbsWZ|9}XfIGk8(<&QyxV;A>g);73v}rm+GYmB$Jt(Ta5{Fk7FD?~kCm_H zaI)7qcDae}mbb};&1PKn0G_iW39f3b+fXcOUBQ>%|9H_^UohLo5J(F@+GhDBd0AE@ z8D%fxDY~t>Q9L`1R9;A*@9s(zmv0h`$N;$ZP-8}3n3nB2qU7N(oDY}kboJdQOOfpp zpia6RU^#xz4HhrQ?kSO_v3X+K&O|C>!eCGS=HV~jZeHzFw&FVk0VF=vv6+^)MPDQ@ zB`jVYvvYTCnCoWtzC1JzvNdhSV$)Yy^(1YSnkw-jAzmcW`wXStxmB!RvZTnd>T}09 zk|0YFLXpGmvf#{=j=HtiKblBDv^sd-G57Aj+1 zqgwdN^m9^@$f4pr0+@%ZH+!3)Rij-hT0Jge(iv>!#j_|R#a_a5g~J*|J)b5VDB7qo zK<^?EN`{zZ6>OfpOCp7k#KEkYP_?lmJ)TdtY)ttOr0B7V_N`tdh^x5PSK%dCQki=25q*X&KrZE6G>rIEmhHBD6mSWE z{dcpd79?i$0o=LUG}=AWN&NV+FZvQzmMVTNt!}NYl&cWV)nfRJzruYcWG;}mm%%5! zXpmX#yb@S_dg2nzLvN4)BSC0t%R7gOHPxDmWT(Uy?Fp?poF*eoiI&CGR_wf{-Dxsd zCHhRsUz6Opw^EA3Fz05c5IbI(TOP);154e2ClT0BK< zZKAE(^$}F%-R5iPJt!5NbGHss{xd9hQ4)qp5BI59uGGe&#neSqvk|QDrD;)YV8)T2p3a2boORx zy)KrN_h?C1MNu~ss=eKel#tA7S$)@(9yNqDfDwH{9EGA79p-|4%$#nc5wL2@6+-&v z?O1N~qYtK3i&D0@Xe|Z|?b|HN@Ra-K^cokoNKEL$R1@k{T@XtvMnUeHw5x3R8cLBm zJBH47I0j?dC}#f9QPb&a@7}1WUOIFJx~Ye!L#vq-Jz-0GbLXnU8N$)(bFWQS`O?!f zAXsLg$Wxt1?wjU2H=t;Vh`(gX{TVy0zQ|VHCzL?_LZY7_c3jPAs(8M-HOE2e!sk!z zP>*_lkfYiM#a_w%+3#bEG{-|-^&h_d=$nrbnOV?U>2rVK6UN7kLh<@!sPnZpclAnD zw4dfF-wzHjkK52(CvapR;lpk9{*ZxUz@z4eLfYGd{shCVdB~7w%@_Mm_te{cK-%4j zqm*mG)LRS+V&xanHyrNZ!ckmRrny9%-H=81wVy5}eTT-vX$IMu?O|uqiixwVsIbk2 zh1{~Ke`flx0Ne~bjXL&nPQ%$~TYRbC*^4p$JWCbyGjmrp5?Rb60RPXEbB&K2fS(ZD zYvR=Y(YGA00+m>{Ckow>&U1=%;p6a^ecCN?<~b2S@{7{Yv!yM6`Kr1;N{j5Xm|9D>8*E(BtS9}i zGuz!LE({%i9J*b~Uc5&2vO$eJ071l7_DWrLR;V*dF)1EuI(c?}pw&evMC``((sWzA z7+>{8cA6CVsDT2!I(@Wt*JycobF~JobovNOmU2q8Qh4rd?WpL*WBr;3+Ju$6M;wQZ z(1|2?Ttby!B%$UzbXo!0IN@o)^_pl_3q#VvCz*dWpOB#H2b5sh=kP%W)qt~z?qcT^;`|fYjOs7CJ7gM3b%C*Y>M#AY0ZJn}0(@KR zXq_5kVB;(og)4&Uv(|X8VvUq@UxPzTwI2b&)Tk}xFg(=-^BRvawSXi;lq!St(==m@ zDGVQ)fhk`l)ks0Ibc1O*0Ioq})fZ?M$LFyvNBQF)FB+6~QGRa}@FrcyNGu-}r}D`z zEfW+4$&oC44hY}pO!CFdA-q9D9up2uVq~fZGk3U?yeYxo>*zm8Q-Y@jm0zj>pr-r^ zG0_A6{q%<~>O|^1XD0K5{OS5+!FltgazXVlD08PTt{8 znp3OF&*$?;0scU6TFs+^lpOMwiN3zw+pRXF@eQ6kwd7a zn_*Fnz9JL0K<~|IK-g|U=Oea5o2tfKAKBBz#j#x8)#iu<$Lo+147Q-f>WU~=)PnIk z-rOk+xTW8WKE&LN(*7DFM(_`;{@kfKWfxr5QF4D`{5=#bMMndJZLmbuZKH$qM+gFB zdB2kmn4@);Ua}lPe%7_`u(O8W&4RzQvULjWo*!3}n~uH*01t|>sRfV8GIcE>zo5^g zim$2OVfkBwk90ir5M3aZB_(dr0ajAXFd{Gg#&w43a$B-zT)lvVbMkjNi?snXs?R~Q z^4LW8wxwPWS}vY2`LKe*>YVXb)KB-T8DDZUTvWwjju{~h==)*_J;@u45ojqk&zyqu zML2i&2`z~xKvtr`+IQoDtN(gMlSq>l#Om|3#BL>Tnu~Qak$QtDm>`^Wp6eqGQ%f#b z;l+oJK$IHOIW=43(goV+Otw(KSeIozQPh(#Mbdq0tTYFs-Qd&ARpx$jrwL5a%O#D} zQ_sje!bAZM{miW9P&0OUcmAz-HMOYdSw89c4>?{A;O43@%~a&HQkHx&W0Mzmk^pz9 zjAl8j!YmmbJC_@ntV)-milf8g?^zOS;nOr0ve7epUYY2qmq&^R^Jt`0+NFK z>xqgqioTDFKA$n0L@X5xD-bJ##k0=Ed=jn&hXs1(V%No^ zxF=5splOhpGrqgFYO-yxF{Tpr(7YO#zABK^nb{@)k)=ntLX|KLSy=%5LZ&-9vwlpI zz&{)2#4GMigxhvoF*o^!GJeKOVQ>fcf}+*;-W{LH>mgTDKnVOK1kH*Tk2A}FfarxO zjGl4!woqppD6n97!tlQ1{>hj! z>aT#3nd5Q#3YiMX)`z7(YlTnLsPRe%e8hYS?=OB@4Vv)~JfnYL8oGumG|)p~JTL3r z^|3Kj%IPd9TH3+WEU`OYIS3MR{rD(dR3sC()D}%zb% zhIfa~k>H;~EqlzJX1e zqgMFliz(JCL{tgZEw?lVI@#CyBwbF^LROZq>5^9(I$_d75Memb>y>U4Yqqv~FE(|y z(GOI&&0;=B)3kjgETarAtK1>$aGj6s5bOVUtJ9jIKxe12Ju_Ak|d@K;=%;@j%xC6o&2aGBt2 zu@CMLuG>D$;jK*V4{rPc(17)a%!O%X0WpGa-F1Yx(Ww9GmTU^(aSE7`*oAw4=I%X- zanc6AS_*g$$@XGoyyCAolYaJF=eT*h_sutOTB05RWdJ|_ixkOYN$8CTui~sLepn66 zXZ$INT#&&!UWT&i)-` zqd>#!0=fIzdFivNudA48Me(o~l4{>~!IcTuCyXPZWgJH9{DhV;aSR zu7}piWSDpMd`O~7V^{A`e1r{Otb4wC3r!Pru7SgFjmsLzfo))KbC8klBi_zUlGfl# zn5YQX!(S3Oz%zE5$bq z0#Z_j9v8%{0H@9XP@X2U>*citc@&NxCX2{f}@a2or0^h>0Pk!>U^|cRriY%YeHKuM*&2j z=gZ0P2CR#*CT)HZ#_cEPXFM+c?EvfIi}BlVYX;W{Uz$B26Z@s{ERmMH!s@}2`a^h9 zVRQDov85&F^WtvU)AgQv?)T*!pCUjS)E635YZ4VE)&Ux#5>$`jRANT$I?TRP}W7LQ8cuXI#gZxW3 zglaG^U8|$rvev_k64`tafY!&b?1R>}$qF zHtc)y{AuPj(C9}Lh&c5o+Wcsok}B9DLO86YVJ>4H@PSL*eHIKMGzhXRgc95K*MnI?c>)LYa&&+TodP&^g%3 zmRkCuiC{H$gQ7>cU7kVn$Dh$!#-^p5k|=WZ(NP49wmIiC@mZ9o#Lcafxhws zKSKw!wIN-YdNqm$i5R=*-{gNY`)`SlTgp>l^^J{kciU~PRIsb=Crh#NjIAvk9pZ4 z>VpI2t<%eLxGU_)+UFQmg~k1ZwbYjujzj_ZV)i;{*}=L*NhZjbTWSiZ^Uvj(C3BAy zf&qh-is$K=h{1zty>w~|OJ5=?YYp3ps6DbdjQri?XeleS^dwny-RtI97&pu1pX?7( ztl;F%3l&+YVx{Gg2slYjVl-A<`6A$L$Kt;UOC(~lhhm*J+ObuiyOQs_l8k!SIS2vv zJKZL))eZ&ROodUYuF<)=wWXc?lG9{opEq4G$*nb&m4Q`;Nyh6@Eij(cRNGgPwWiqE z<*Nl!6_XtYc1$+wv8sRDz+cuG9?@%m{-()b_EOgub_MPeZ|gPt7m3znjxbhrYspXS zHJC|NzOx4m^XNNyaiuh)sQT15qPGINIl?eQBi6;>ddr?UzwU^jX_&Dc0AY4?kz!*67$q2F97QdN~s8jHOr0ncqyb+UjU!p;^1i zK$)sRGPwMY40j8&tv8D-cQ&2?6;t$U=}03(-Xi5R6+Tu(i2z5<`*-K%=sVDuICA{> zGlsav{9+mx_AUv|w(uO7IC?!-dV3h#DzBfp;5PZ2!z5|`x`;ROzti5%|`@U?%e4(< zz5c7{T`V=jJci!N430B^pnPae`2kPHk-4ZHLv=NUer%UgXzIse@7!fH$eteWp~jy~ zUE%z#v`mi`@MVLcP3O=aUIb+BA9fBFYn)FP#W9%Zs4)uns$+5&!l%WI=G4d_ z!S$1k5{3RzuRvqE7PzCSPMg;lHR0`I2iK0piEnG{r>>?C@Ujtrvg6Q}$-F6eg?E{U zqK!o=W_y10g}24YH>|c!;^V8V3=ga5n%;As)tPz{JP4XB4oZ94cz`i;EHjJlYsq$f z?5F@)0*q>tZ0kl$tVbC=Z(yAGegXLHG1|{(9T<`rf4ga>TB;iKS_;9#0WIsi)7ms`LP0LbO3#%E4n!)TL|NaPRIlV_jfCy)rP|IX^N9M~GVynD(qNcDBfj z4uvK!%n`xdw1vtsD-1A}j(esx$123?7C)VPe5ElE8KSGH(9W}e5PscVL*#MhxMNnr8t*=z&!*3C{U!+N{$+NM16sg9@Y z%rMl^^yL>R@y3>T^S&=E&QS`!>Y5IEF{O zJAuzsB`C}E0)3r{o6p;U`9cAGqxnesKm91{<3=d$fi*3cR zAN+B&R$*+oiu&XEZpMrESo34;br#m<}5(+P@(f=;kE7H?2nM3?LDfV>@L6fEf4>Bgihq~+$d(0qy&Hw=HO<(9hzbF*KL`R&ru1HC>lH?fASQ`Uo zXd0Mmw=npshO%+wrrNk8=phE^Fvm)bi+JDEoZtm{n-_O|v%cp{ceY^v&CXAAkRi1B zud2GAA%(PuP~L;?>V+)ZohSU;4;F4iuXmdH!{(hsTj#tO>0}An4CpqVEl7?(TRMQt ztk;k?OtXFVkw=~Md-pDoRF_P~9wYYh%0__j0tPF19*>G@g@2C(z;Z&ODP+AiVF$F!uWKu7ETuplj+{g zG;i1{mLBCstxa?eyXV!<_`=>j&SiiPn0Xy*&8?Vt=trNug!V#5cC%1VaH^KPHgRtj z!Ol?30|&*-&CbAZh^gsZ`dBV~Ts=LDFRCJJJT0~HhQ!qxo{#A5%Zd@_Q4vcAa>B>; zrCUP<2;S{0(N^N}?!l#oRmuJSkKq4S!=X5(HO>Dtum~0?5D?9O9A_0iYyhMe+Bo_T zq{7y+jXD=}>2NA2>`LNXU|iELByAL;M5IECAp722jEnmVjh!3iqjZysji_a*rW7uI zg(I|FF3Qbl`D-*v1y7&5*^55j4@*yXo?Uto!+ylsn_lnlKJPntMS-XL5}>u1IGXlD z26Wtemabksh6bg38FX+IJwTu)HP`WY&9_(FPM^l;l~V`d+xvqLxTBIXMbp&1gtu2i zH0R+g%wBQv67YcfC_=CNqhF8S9&v_ebtmGT%Dn^l-NSJJv`<6yiC01JTa8v2Vx?Ai z3k-phGHKUN1R~9QA|YaRJ^WR6;`K{V{SE0H8L=jZQdfN<%O{ zuI7hVp!6WkS9G8XBT#3;ny>bN?YEXNFP%OA`2Y@Xm|9?%9HBTl%qT1IPaC68p4SBU z$q%lV(R)sw;0Ek400u6`JJkGe_m%JU1^X$Z1{#ha-;4ADN{`0hqoNCrEMWrbixCHq zD!04~+xlvbodAyGfS}rGmubDr{og<)oX0hHS|l*RW6jG$x=*8JAz zPWHlam2e@dgxpTmz3eIq`LX7n3qwhNuID9Gs44`ReVDhHIi#ZO7&;|F zV?|0PGBBA*zJ_0BXULg9iS50KM|WO|4#$CYeC+wXkJ z17mC^3AyT8+(7xf8BtpERD?$XJ1Ny1Wnc^z;KY&c<~v>$$!xC>Ccu|_1sas*-crKs zO9@)TV}ci_`MYraY|JI8M!r4vv73)g*kBspXs2-q@D0i1t2j>ottMf7^lmDr`;jM& zjj7(fgwAS2mdqh>(i)pv<25AgbPK93jU~;R7Kv3SeT4NV^VdPet^2>^O((><4b~t%j>DKd zPW&MSK%)d!pA3FrWZ6xukgT&#%941 z_iqAi<>A|VNVwpUBFs%u3ip{CW%~DFq?w`)^fx|nTe`^CaQb!2z6%Tgs7`JNo zjPQTQ0AT4poeipMCt9(XbH|OleAo$Df0%3o0j@48!%xmR&qu5+yBzXOiX9TVDbl8~ z=b2I=F}TH>v z_p0p5q1###^~Rm3>>#6?WFqLJ$qJ@jnTm0mVo?+_;#nk9>`hW&Xv#%VGEG{@vTVxa zfa;4@?pxY#{uD+?6=zx=3p0c=9AR;~)e&(O!+quKswi`lu3zmO`~;kJ3ys2_vCPOK zNtyX_@yjhmB^D&Vt8q=vpz!50O^cv%c98k4Lv7Z{Y;!Ejtml(VB}SG;V4759;H*v3 z&@kb)IlHJ2^-i<}bm$o@tlqMTamjxd147Gdz|^HiBdE)kYD#U3C2AK(bd+fuqxg1o zP@}g;DN^w`Nft|#O}WtG3L*e4B$sT;3AWn#{8O-`>lWowoZF(y0?sGwvZlCbi?vreO8ik|h&ILsOk)0+x&sm8q*VH3o$Ym>FaB z{r6PD=H8f)uDm^}bsoM7NbMJOfRO~mY47EpoD)U5mYdlNpO2<{0&&a}1nN+IPD;}T z4dz`0K1ECQ0Np^74UGljh~DNg)zIZQzWLtnF#5BjrykyEaCibb*Pc5D3MDxNarBuP zSjF%UZ*$Am`~!P8mLjT_4EmME*Gm_(6_hbAxr`DSlF5)+_a~T=btX+-fS^jYAqS{^ z%FReq(wep%QZ4LF`BSzwzdvM3tUifN9>Up?(sgtfGoIi?M+R&1!)E%p`)TS|^}Od8 zjHH5oRuv~JCog@6?euf$!L4>BT}mAnqN>8g>gL=fu6!nCo-Y?uj&ucT``$#Q@qkq9 zF*UygfPRj`6Xz=ZPl%WifEa5Tdz_Xp*m)vkj5aZb-|B|fj53k^GW1GluhQU7+%x}S z_bk?B)jj`AMdv|ISYS5d{=<7-qkkxbkQ6y8tJ$1wCaH1 zrrnF$w@5E5zRS%Q(+5$>@Xp_*dmJavIiFpRtW)?Z=dq$&qi-M$fH7!0@I7(lzbQkp z{_Gxbs-plZ+Xfc^lda*=e-$?{s;qds-+t-y1Un++|&`FXqjPhKIu)Z$a12j$f`&y78iYgkRJ4{#| zBw-DL>$pN_AFy=n@8Wm~NcNO@{SqFzxn&YC4vED+$VDHC2-8cnq`2RXAx0wG2*;Vj zx%vY5`y7&GiMjP;tw3PR+nlNJ!wQx-9v0I{I#^e(z!X?WD(tvHcE3(*=HT5zdok`i z())8oZj?j|0S+K9DTN_5dP=4kR%66vYH(JP;5_Ha`0dKFcp9BaGQWO!EO5)FFq>)b zSvXswC+inKJUJ)_cv>E7HeO*Tj!xlXG%y>cG`Qm2zieUHAcw5=Sb|=Bpl(FT|77um$Q9Rk0}w5GH765y zqt6oibETj3_!683{OEhS1!j>j?mYv$A-0yR_LuL$WbFKT@W?%w+&w+mRZ{fhg67yf zJR5Sx02`#tYd0s#n#-FQ-Vxow{jR>(Eq%S`{AUyX44ScO4H;t@{mp{YGB(7W+#Skec$;N-TcUnWvW6y?D-`5RaF{t#xpYD9wL!Tm`@e19%{gSR zLI@zBR)my9Fr1V=2}FRVttaj(`tCZ8b~K+f9x$+S1Q-($?S>2-G?{`3DsP}LGqJ+a zn7r=T5>WzczOp1KWf5Xw?-1z%NLi*QA(E9Wu1+miDSU!QUHxjnwPXM|pwOH!&TP8* zh~h;@;BAlfEWhQB`%~a~{;!`4!7qs~f9!5&tr1T#Kh2RhF+Kp%P-GFLDYBVqBnf5^ zwkVt_inA5`6=DLg7I?}6g7To0vrrAd4vmL{(zz9e`2zKyaC<<^F2{7!7d}h^Wd@60W~fEU zmJXwja*VPkzS_c{UA&fl7E|t1C2N{=CIm_LQbOu_bO zF%<0UbWNFaae5sRmUL>q%{5K^%z$Pl&2J@z=p7Sx8a)CmmQN3#1xa4-zUyt{wcDy*hguafRbcR2Mn zhR7Zp=xiKsctm-Jk|OZlkhfL!_=# zO}zX@?J>treF-AtqNYC;KBH;JP+DB*rh4;r?#0DBx545AZ10rMOQ)op+;k^m!-W^V zMJBG}WtH$s(XZyXZ&OJn@*-WQ+n z2ob>Z6S<7+S9+}Y?g~bDm5D59etl-lWVBVzs33()vGNkXyo)^};iNH6$%Q#+==Ozv zmemD1C#q3I)K2z`XTR*Q;$3e1>|JDhf8->PuG^HFAVofoT}`XS3>n%bU<%I~t-jw0 zFL>6c{4fXfcm7a%Ii&b9eZQ4sSaS1t#u-2!&_LV>awNt@z(O%Au{B3%akPCHaH+t- z&5lu3-Fq-nc?Urgc9k-#P&am5F-wE6BBL2J4ZmZ94w_3F<$9*I*e2WGwSGUIg+-df z9>ZtJd0#rK_XxdZ_OGXAGYg$H^Ff45F7ea7jyl(pEz@i{Oi9lwz<8z9fKjZ zv6rbM2%6UI4MAUZiyf*0Z4aSCgPbqSm1dPEYNk{6-%aeP27CtuM$L*h^UgGFyn#~ZOx#A*dA;e$PWOiMMTU1M8MM7e3dBxT~M_w zEvZl)TN84BfVlg%C4Eqd7YkN6&bJ0Wl7*)J zPvY%*G}qn$vZB2DMv-cs=+)Fdsbo>ziDI}e*UUc1iCh4x*{t;Asi?*) zU)|rJymwBIL)zyWE(kTqeMDR5jBfl88<4lkwM4vo9)bi`&hCn0~2fGUVt%TI8yPMdS`+myAJ{umS0an zz?*W3GV4x%O28YW-fuwzW%T@TRr4J`A83+@8lcl1C4$Ng!V#&mZwv(W{D$ANbnb-Iv;EJQYDh#MwaU*gX_lh044yaceI9k#;6VoQBnSR25;WK_gR_Z z-JX3SL9Kvk|2(>YL_@o`0p%lLrx;irI{L6RK4|CWQ7~K0in=PTD;s9;`LDlhLuM(Fnr(%2y3QL!b;vtf5R6ElmAB zF1C$D!LV(Fp_)=Yv8r;{Szb@z3{sX%hDG1SDLK=}3!hMG12DG^e4$sJBc# zvT@Wt6&yYYstQ^0IsKaq6RI+!+d^l808Qz#C_-QC>_gS$gsyz3yeCDCgnBFyDqE+5~sB9wwr-zp6UM3#SN3(}8o7q}dNGOhwv3QX4RvANGEKcl!bigC-PDz% zXPTpIchNTwt6ScgyWYDa37||6LZ{oKnwd#(C7i&-G8+)2XST)YsIl#&*q@DxFrbmv z3CAoNgcb3P=G}N8GkLeHnU7-sfCl##E_2G+D}1aiwRd+@sN|~9=`c<1Oto`QAMrlI zbGiv(VD|9&g@1@mo6_}5!6!0NbEOMpY62ke^qIcu7FrJ7_wmRZHaUulb2Cr6j0oygGo zdObDyN438NwHwJH$570~?8E7-C}Q;*2$|$JvFg{^^;eR)xj5PMk2ZSKMoLv|ZRtAC z$v2zR-pZEYt?)%zD4I}ru-nWdT`rUo#wJYHa5bktyNHLJj#(rPC|!}B))9Oy%_|_f z!FV+SN*l?&sITJeT15e7ch|%-Gs(^4up(LjPdYEvOYyAxfkY#w7;K6?Qd& z%T>&|*JEzZgKk8ThAChN^KHmsnc4RwoicKI{vjVt_o)3HM!M1{x0#$c`47XFs(m5E zJ&Gvh70k-=3aklvs7ucaS&?I+rm+OWv%JlQpKLKiNp}LBg^>+Cya(I5nu>M zHpp0=+ubBBEZu!YAEf+R5}s|V0O%KDL3h-IS!Vy^r@L08k8(dT#`qQjF`G4iL_i2# z+7x>f3Nf1%e+=68?t5if;g?bBJiNWRDH1aZ-CfcXtCi&kOHw&bW$WgM6QRYk2lH4_h)jqhRo5GGWCe) z(3b}dz9(2DbS-F;uX-yb$y4;UKa=MX!C~;W1Uk88TD4(Slllx*L+?(o z;*W;a=epW-UWJrc3g%#t{XU*tr)ZSPtnB0H>J94oo zulH&4@gNenM1Snype=J^79~es@uvVsj;4qVs?m?#-3)YWHSi0EB2Du2x#LEVTxl}5 zwVcS&F#(0EOiexcGP=dYSmSo;Xl?I1>JNzR_KYg#tOJE@I)<^4@5ZVLUC52y(`iV_ z-|7$@tZ*$4-fX$GudIdvc#ZfF1;{sBAfb8!9c9+-NXF;B8~o2jISj=Pu3%0UB7AKJ z_??^P#{GP6aB@#FYR+75GNdIz?rS=?OsHw+r7K--Z~?Y_PB-R3l?YLm13;NtOtMBW z%Zjwn$Bh@KWzDM>Y@RgF{KrYch=Bax2Fmfeh<4x2Ejpf&ZjV^D0_dn%sa!Cv`@Ct~ zVbXoxJLJt3?y9mJBnZnlBH=Ex+A2FY%&$%Bbr!?c2Cj&-DQLG%>SKKcUAlx?17B1VVApJC`m|2=}q;A=w3WDqye9#fVRy9bO?7@DOr#AXDi z8#m4f#s*PZW)5@s8K{wmnh-*8PFQKr4yi7?%jPT$ax02^DaB;u`_5wUhL(Pu>P=?| z1LR(qH^hO_XbQnrIjG?|=*#yvsf$1_?X|r6A{(bIiM?|*%VWQ1?Pp2|%RUfXV9Rv- z$4Ll~ISaZaCF5GNfuNEVrxBeFhmms8op{9;!6m(4DNy&jt4^tGt%8NeG(h>EV89Q9 z%lmiK6qF>rAVpD5t`F|U_-^lL+~ROg>E&CRI0q-Nn;c2v)_Q_gLDBG@U%{q z$8RQM6xmJ-G9}?X(ps>%!m)Qk&qRkcU^I!pm6#<}BpO|Bu!>4X$6}Ispir2W`ehs| zjZzP_r@;HHf6{&~2g<6POP2fT(U_{<&am9$M!h`Z2BA2?HK1H(Yc}$D>GJ+I(R|0u zR)za@3020~O)sv$9AyTwOu%^%Y1|e{9$$tf=5-a7-RACD^zLxc=^5!5!t7PuN-> zsY02gRZ)VB($9tz1g-pv60GtOF_2S03jY0~c<0Q|{~1p(5C<{8hDHHV2K#D?l3_9_ zX`GF)sj+iwu~$}dve$ERYdX4NsIqgibMJr`G?_4TprJ#I9Qz<+P#qu`ON#=;qatqK zLy6sbk-%(8XktRqxUE?L+6ilWb?2I5>cAyb!zEPi=T^WI`N>r=ZvK|? zh3M-d`sccU`j3S#{a;?eJg}!qoLx=rtGY$0Nn&XRfG3B`HO8(1+4#j|-d4RYF$@=2 z6M#yT$Ci?!W?SZ)Oe&+Fow*eZn(~S(%wP1Lm~*XticnFd(IWMGS*fm!b@4go@Dy|P zA;hF-0}|FF6<6J0JG8G{e_#PSa(zXthqm(*b|%F@KhAEPMpjJ@O|>Bt?{Mqa zqk<8&S>S&qYIw9mVV_dqPTjrVqEfvdzeP3WhI@ZfDaP?IzyD=7AxD1pOO@1ypz13$ z(#jHa7Ksn+Z%+$eGeiW-cVgEh+syq@bFmkM!~6xN_8QWShlXTH#sGtYDZyA>=rwyz zX$Ze5Kf2$+JfZYF?2|r0<7Nyz1IKv204;z>wD5n(EDcUdRcQ=Fuh45u@h6%IgwR4f ziXyh?l}MMEoL)KVqTAco-0ayJcTh$-)gm=kmvnB-mC>bYk)^1hrK5 z=1})2XZ5WV(N_hHywdNg_JEGTbciz*vhZbKm(KNZG6&3v#~IFH+pz~CINOuftJvru z(VtCy1pMuZoP4BP-$rlYpZjT8w$dy6@q|@uRBQVg0DL6-51m@$UShQ;_ojL%?D;4N zx3iKv4BbWt9h)!AD&@R4^_;DD0p$#)7eySz3u@P0O|IAKlE*|F3Kr7+oAo*@x7W6BU*mjI(3qO-kLu1XLdy;W zQ@C(yp2wPL6(XOMxHVZT>{$ys<`#cOO_3`r)r#KN4Az|#h+D`ygJp4X8EN+aQUAO! zL09B>kk{sNB6SvFrle`gz95d0oP;mYhM}d}6+y^2QGT(tr-8q%%@jmXP)k>~ePbb8 zSskRm9;yUOfm}~adf$X0HIu#Mg;a;n^RO4v6ko2P^4)Y{uD0nFU};*SOI$c7v3QaG zvPM5g;pB$kU=A51`10mt=O-wrq*1cYyrRHr7CviguD4XD?pvKnv*lSd{q6T|D}>N< zUK9g{{YG7#_26tHzyl~%)bWg^)d7onJTdseruq+nU7W% zG|~!!SSJ>p>B%=fu6iC#Jm>^y;yEsFugrsY4K2WX zBro~8&C9_+m4@#cXP=>(*3;}>SEUY2$v+uZ;5&V<{r-M5Ru3t?jEo~qdW+pXj8-Zj z{ct>^9#QgSJkkz?L4skUNf*UG_25`f-0H~y37kim(p3-Rz=PDQ_^}V(f6Y!DNm13N z@QDhvy(pm*bpX~EN9?)7_fjI;Vx{Dww=(X{6S^=mj;e3XZ+R*|-|+atZpf_as6c^r zre)0ll9G)jAj3il)DJ$RDqq<9aZfoH%3Jy?K}CBf3W}r}?)kv|f!*|^HNFLb$(`g& zlOFQg%b<6k-W?%syK*R31;5)wjQ?))npSalu2QL6BP};J{!Gg@vnfjRpcX?G(Vn_3 z#G4jfwk12mP)08L-Gk%v8&PV5s`5dgTac66&RB=$NvnJMSP)0|E)1iO8`n13W7(Q` z5m@Z2J11z{nbSw;?X`eu&ub3>^AW`Ajq zzTrk$wqO58C{2J!=z^rgKS@z1UPxg+_wA|kuDqG6$F0rTt|e#l!|5Hn4x2x0L{}ct zhxa5&1!(uA6Y)P=l#!W2dT4#qT;0;}l;2yV@X!krz*|%y#i)f>Ps)&T(sZk3j!xxj z#}e;?hNcuHKJE>i!&{-vJ$qg|b_<`G*DHSX%qKvITHLPbN9tf2{t@Rrl7#k^l_EcC zce*U;fZ#G*JFea64bcW|+5=$TgZ+rsSY;LA`jzm`&Vf=p0-P`WlG+^AN*$(E=1>(E zd26_(sVlS|dP@9I1?92}3+&%ih0-47RkU4#w3Ei3)uv*i1ZJCY`K?^vom_6HmEPPp z;|pb8TYX2TGjub2#Lo^2j<%E(Y~hpI)h)OH^^7g;B`cgp@Xsu>p_1 z?5~us+WwM(kd%Szg;BcX3o)kpQp+~%or|QKB%n(YTz2*=-g>E`a=L8tED~vdl_;4N z5F`0jbs;jgOq|)3{#tA&r^3S<16ceuaRliBadyCpBzG(PiT%2n%qN4@WJ_Oej>;2< zVH91(H0FZI_)J#nTQ@hMDY^H#Ca%z3|_6z4q)mM0%+1p09XDvo&;Hq&=|Ro*Zv7`pz@u!+UpVDoVDGNJi&S zOVS}Zpa0LGqUPcQO%yBpx8~;fzyC{S!Ppa!9C$oN~PXkYZ9YK(#P?n)yn#B-_YWtEfy<`qf9((z4Tc zb|t}qBSQ!WsAI&cG7$(nhUt6W6 z4hZh#R=k{7YdK<1_-~{=Gx}c-J(v%JR_ornXwX%nVPSdq9s4w;@Y`^ zzf;5ueKG#)(pJEq-_Hlw7;>56WOT z>YX}jAoGThnrMxbN}atMd1jCEwp!M?ema43UH^rP?(hYVVU|;r@`%S5<+@h6j|4WY zQUw=%s0yylugwWAnt*c$E>-OM{mwr644MKe=7|rM1#$v!MG5zOs48MtrGanLWDh;^ zVh(xiL9r*y&zcqoY@|*Ve3ltI=B#~+l(mqzT=gm2t~SFzT*3n1e%qp7m_tn0^~|~C zz4NHi-hH3maE33K+B!4vH5=t|BDx%UfLAJiWM{^1yDJYV1&NA?5Q*z9LATnoZ0*c4&%8A?aI!Evaqaq^)W=NFn8=BWTcu z{1&AP(-x3E;?Acx^r4sh+vi->vJdptJsV+MpSn>%9AAkifm4WPSdAWKj%b{SCx-rz zaM~@c=cifiAA-zrzjiOmNz{+vVG49v4OZ5ERDTekWmCkku*Y?pOK#&pY7n$D)I!7c zG!(d;<>C{e=PGU%0>?qu+=k}He*) zqp>`W8Vu#j0`L6#8r*007Wg=8A5AGQ_vC8(!s)?-p6HF}*u2Ebc_H}!JZ&*}?l?R*Vr@v2i2 z2GS)KVM>Mh>3fIv*cPZx~ssWTs#JJ;YxD5VNH(@$N-e-Y2ZLwGrMk_^YPJ2LDX zQuF@xM7(L$OYLnsztK0v&xu5P#QTCBp6wk=NH|2gNT;gyphU+I_h!EV5fQl%uGm4u zo2RnaAa;l=;_jSLD*qVH;GZ6a_v9xo7)L<(?IBo=BUwGIxD;NmMQ;GuNW30jh;0Vw zoi>H`7}4CVT9?-C+b6R^_c`)6zeER$AI0?p-`U?w-h&99`HI+8OXH@)7IeL3HyLxNr>2VFtqiUk^4_l6GW0#6Agg*)Ae9x264 z&w6|Y*V(2-5XHJK=ZR7U~=lg%+|K9Dpj zhq-ppXAWTa1$1OATfk2DER`jyJC2diUmPiLq7cklBh zOJGbmOy-6KhG@SE$ZFXHqS7?CzNIswR`9XQ7su&jziNLYva^O`aeem|G4SEe6FIrO z^%54ANmKth8oDTz6@Nc*6px?kUWY$5U6~BrVtq0dUW)-4f2mq9zcripVINoifH@wL zNuN0~GGx+VIe?ndwoW~qfGsS!!|JSomc;sC)R7;DjaN?>6i#Dhqg0B)Ae&;BqP-m1 zAK{L$b==3rY_1M;L0c(4A3^NsuCRVgG3~ulBncg=;>W>sCFGOscH$xm$xc5uCt3uRV$0=w2mqShA9nmQtn z;Rk-!sumInqSMD5Ba62j+zmvKtma=)=yu#(xl8xAx-RID#Xt&Ixsc&-_iz;48+zAx zm-gMU3-6ZubKb_f@3$P;yZwE#H*DWq_oMsDqQ$XLuAN#Pvr5MW3pI4aydbQgz~*lj zUkHXpDu{89;!!QP)|)F`9FxVU;hH;N;yH z{=`ihTF~B9ePx$1+RxGBrSo)iQ;8%Zowm7A(thWcvo)av=ndK}I|iLmtYz2~6^^SZ z<&^StEK{5eb3VEYo?%=pZdJ=EZTZcYum+@_nfbcl05S4cD=P}58WaoWeX3XJg%wNq zYJ)i37a^sNdaVY*Oz5$MD11`guaLg63~%!$Mu7avd5cEJ+`>f)I+fwjG$xxIGiZ_) zv*XIguEgW?k}xs;*`{*hGuazek+LMHrX82(*$u8-e7%Rej=Ub75!qKSJUqr)8TvoYbOae7nE3zvNq(gQyZA_aZq6^&qIby*8a5jQhKI??59(l z*7v|-)=%D|sz3OfST=8<9>c}x)6Zu%98D8FmO5gdDbeu_pKA^O5h){Dj;M%Ev}B6P zb>0`fL{CjSDM0#&SAv9F$_tjAP=0=xr(Zdh>cwxO_% zVJk~=P+T%_*!#`nu_@bH_NYG2%VJG^=CH6y&nbLAG8{8CNDjPG4=>Z&Z(xqnun*UG zAtDj;?g#RH^>M}e&z?)7rl6uj%FA&$5_hp3zAH6xwS}7PgB*!`L&2QSGIKX-s%knP zysZ=^Jt?C4W(JPkZzlNjC4Js_`Q;0Q;wv#V4C2hQB<^-%f8J$ z-tM$Zl(BYYqg58FvkEFqG>f3Tsl4)TWojOhEjnVy_irYh4rQvyggsO5i_nGzWdkd0 zmrjp!hDoJHi8gkWEYf7t8-80{krs>u%hhD4j8&zuqc2Cx>z9nCwT-s{0?piDSHpsS!2 z8wOgkVsTaDGEic5!PSwg(v;F65m7k4#6j3^sg3eOEWTlsqIiRpp3NSU1&vv$oFOOF zn3SzA@01~Huq-F!NprkuH~lr;h3+)3m^v1S|AF%$B3Ef|+M~5^bG!Nqyzk@%+a?=p%b73E)sqqYf;Sz} zN^S4(l6Id|a!f23zX@Vpl5!<(Z=bCtUokb6HI)_EHXEA$9cI=Gr^Dt>zzb0XE8#+@ zvX|r46{$NX)`U0w{l;u3s6*PGOg2$@=dCJ*2vX_|1BZ;NNM>9aZ*SX&A1GcN{({aN z)Wj7DO;q`|0A$2>M9rI@{>)oY=INcRSxv#DAwtMQHib6jG%WK+;y5 z)!zDLQ>t!Bc>+)KIS;s@<>H|(H^Og_`|sqvr);pkwV;nhU%C--fqHSB z@qGd8{^Yvvx2GS>;5Y;jX6Y%DvFelITSleyFxX1{DQ0`dN!Ix$+;JayP@IgsrDbVB zP|sQCKREXUmdoldr#3PfDtmacJdzE&=HI<6za7ovr>%0vZ7Z&v3LOMttd76(947aa zBtBQVueW*p~9pIKPV&pa>)hhF=wA>nxK*AwrPvNOj*_g@q` zX))SN1A1_-`I*{+#9Mh&(SpCCR;tQ+J48b6E-?)`=aT`GW=)PNrjiSFDTy} z`+dm2^;IUClD#r=3!=wTR3Q@fJy7kasNCQ$3&1O^)SYja_YA)Q8Be0l8Fe0yUmg0o zgemMmU`7YWSQ-?8W`4cx;aXu_8EyTw%myYZv7j0$dbNCQyY^Cq9<6WYr@dufIes=u|SQ= zgAP{VB|Ra^ku2(GkVMOZ-1-bM8m<+IOv;HwihSYxpDGLP8!Q?xCjif6sBpSBQe<-ZDz!hw9J@0aW+B_@M&d#7`c!tFR=+m5uj zDr0|igm7OZ&-E%rz|u|xlsI{@3((M;Wn(wL@Sy4263NYb!8Q_hzx&zp3zEUKidJW` zd7JmEJDY7*kbX^dX-s`SAw9QIPICQCRt)BzpBWV8 zn(KAiQx8ixvM1I_gR~qxp1z+|u?GsA4Y|hxRLkx+fKX!XLd{I)7={nw7!n(cSENtE zuqMi1#(BF%$x@Xts7BmA6x9_~vc>^2`nRT66~xu~TPRcgJ4Z9{bVR_F*`CT$a z1>|oh3d$bYb6}+Yq3q4>&Kmh1jZ0*VpJQHne5>%4#DnE0BH0;^)IfA>%W-i~MSvUO zkxyQL8y{xSGv@LYevE>=tnX5&iz>8P-wAHD-cvKIg(}{iLbvq?LnChyxQS(GSZEcb zN08rB7=P^hsnFrBeKh=HGM(^em+JemGuodN-zPxE99WW^M^^>YWrez5v7^KS-7a6_ zX`hHn*w1mPYZ3`5pIb0q))rGCh6h=14+>xYD5drNSV|B{Ds9kM5%!KSdnHOpk8hD> z;)SF){-TKGO~8N!#IWqKG4Q&{wVB}9l_)dFO>A*IyN&!p+EOg^8nKqI*?AAJI3D)?O;r_!@w;htPx^_`W@Nl=$p zG1TQH@?V$NS}#G(Szh)ZwZuaGKa*hBMKAvl2$qlkkzrJ-{lS@5$HP5Mkqt1Y(aJ#G zj#6N_dcptCRawo+^ZH*`rCvQ1{I5e|z#rbKCrlee zekbx8lu&-1kMMLr4EUkJit^Xp?vb{0F*NTb=$9J${m)hF?*_%(!V_@6k^Tutx@oY~2X*w_LpM=!{RKoqegX<2Q-ZY{(VxKo z9yap7tYnx^;MbTya7PpR??)JK0>a*5|B}ZyGd=15b=bpSnLQ`?g+q8c`~j@mi2DTp zKVkU+^nbxSh@fz4uuU`06Y#IF!+!yZD4)s|NrnOLX#P*Rnt`xRTurFyLA75D-Ept1a6hV z0u#0VW`0^42?GFVLie-s|HbUA0R;(_cb@IdQt*O(AY()996IyoyVdveS6pK2UpA`S~fdl@h`4M7ZWefgQw61V4Ot;)`cBO70Y(nHy(IUHl zSJ!{1>7kA9@17wU0|iope|FB6P0?Q}yxvCe!Z&!p_w^86dTf;Ykq; z5K8`Kxksp?z+Z|ajbOQQq4@ta9@c&!>`Bv;u4v`6Um5@AjDpIgTcIXk1gBKvgA1W= qxL^I?zb|(vC&gd9OC4b3dMbj~3JB0f3jlCHKiJOz0K?8-cmEFVG0gF*|hvk0v_yqw|9^FwpSmw;J^KdUN$cN1299*I^}#cYlvnxAWwt8jfSXK z1@7av;e(wmDAnM$&44C6W>=JJ&j*HMJ+NS0P_U=})>CF3mXwVFlDLATwIf5|Fe*mz zwBVxfOFBzx`5(6=B|+cllJ^(!zkp}kaW+Hk@xxj9DHPd*hSHbuClnTAHq(yuiNGnClYNu2@!*pp(^R{)S4&Swi4Ib; zTf==|)8nxD?a-b9@K9}@)3oLd@R8Lqcv)hxwCGMN(Iff35e6Y1b%$ z*y<8z2_k$jbx144ob(ZY&&p>T56EwhQY#xc9)O*eRXE>KPM%1dKuzy&wr{I6HZ)+c z>&VzA|5`2VLDwZ*R1f>$aLlNd)5j6h4UDKVO_2Kw@()O`{}0@fg27Jz1Dz&&@FI}^ zeL{yqf%p%KOzHwU4gaAC2?7X+{y!*^N5BOnZrLvhB86HcamwR!%ofnop9vPk>Rdv! z11YH`v*Q&DDhcaEWKBv%U(u{5-25I^2@nk)21o1{AvRx=&!WSPfSsG3ofUZUy50l$ ze}nwy=2a97&_;;nN)LL$vsDDdWy)F0TDO{(PdVoO-gFtFa^x8rf8hfOIn8*PVCDnx zFeB>JtX9;rEUg+eSv_8zbymz*#yGjdwA0ilo#~kXqZ@p|=h6!rd;UkY< zfADp$W!75sVf$aN5AakiS+IQ7t_U=Oq~#`=QgFM8fV7kna+acM_Vn8MBUBIcBTOHy z-+k;tu3f#52-dEU_iG47NlX*colxd*i1ls&<$E=+Ii@ z-$mXbTcB6_3Fud&$P*l971L$&Hvcu6Z^`R&jiN17vr}e3pHv)x@3FkO<-ny3yAi)S zJ{h!l)^TZexGKZxLR|x$_2K)zL2(8?V zEl$my?nVDFGMtt^2U;+0iFxlD?G^Z)`2ZTOBp*X$m&`&MubFMu@D($GxxjEhBxnRd z3eI43WfrY|4~#d9W8{v_cMHcrDI)v)EYI94a(m^~L?@%4gCuiyNShuv!lRi}xH9?) z<%~%MGqo!!m*S=wS(S*uI@tuM1xu)f6RZPdlW4{|LP8 zfjHv)Bs!v%@Nq_|LF3p4 z^oB!&gWiLK6&?W=558!5C=XW9c>@+j@7cj~N3h-RBy;vr?h>T5h8lJB+RJ|i}4iXPJRK2tZKJWZT?WiM&h3~|9-K2-Lzd@5m zBb5QU^K@IwsrWi9uE5UnYicYi>U1(KV0az8JnT#vdYO!-7HPGzXH7C89UI%rX21;- zxRY|^dg{;0oq`C@i(X{ucraV4Vhb!FX@Wu+ml18qv)MJ*&m=_ia)yGy3uYtAWoQ)4+GK6J%|onk(nHVcBtJs50C#|z z4(hKQPoI}vkzC*uO#CKau?BT;$zh{j&siN_JS3|7uiZ#%EorHodFe_s*z>1oI=2xf zHXqWQy$_C!urv-^49l$+Ct#2gFUzpC>(9u}Qx&v^Z6sA4l^QILmFv!$%hV}XNe^h0 za3o@h=8us#%d5A9tIC|*yqTLsHWmQ4L+TaxZY#x^jjC5%j1x_w&hPJ?4NKkMNNdRm z7;s~{N?u9ByJSVk{KO_`X{NfvCM5o3q)~)Z@0_`*4dIk#zF;b@X&&CG52)QVCfKyD zRXR%1JlY-Qnauu^>;gk`5ed!F2KUxzeaR2Bxqor$U^t(4#b){aNe|nY&F=wZDYwNb z{8A-Xw)yG$y#=-FB>t2~Z||Txf4_#gkO9dLlwVq-6#7XIm|tpwX+Fim2a8AA)MZ=N z5lVft5JjFr$Qs~yaEgJyb4PEhi?+1Ib{;GoMja3t^dwp8S=eQ|jP3BmX~B&H?W51CyfGsCcy zx6@1{&u3GoQ&j)(UW74l+3txMxogCLx)p)#h4WGEekV!*iUV7&lNtVjqPJ52eOKpx zZ*RNFhWJ2VJSaUpXa7Q=M?krS7uMg9FeAEpHjcKBd?+5=-tMGK3MqhnQM4|HEK2ro zLEZXFwiyiEp7F_X;DNlH&WUS*wHFV5CherEEyG z6m>hvh0np*mtTL&n-+PceU>e)zL07N3d#iM92+4H*w&thSf!uAK9LF)nwc^ua*;<9 z3MhHf@9}x%Q*CJ*f=n;ro7uXrNi_wtyYa>dp1X1pd{%oYbD02i5DDZ4xpP0tWC^)# zNfk7kHc%%?B5c8Bi9;1QB$&cFNSGgWmhe8UMD(lkLbM%`Jq>=Wu3?tt#sK&n%)zZz z$V^)kxz=0Tawn?Bkf5lETSt%L(ApVqZocsO^n6u_20|%3;#3x_LGFgB3cUxxHI=TI zsb&f7fv0gL`c1(3vK+5alRJ0S673DIQWEhWsX`)I|5p-)CMd1Nt69n;h`*?XXANY; zwsnV!@L^@f5`zwh=f*yj^;kFz+ahn~G8ghH6SM8*iMtovl_m@z1365+?2XYzn_?@Y zcPs+8dfa1_62^RQ_lmiaS2;L1wS^9_V;CaACw^?hJVyXEyuE1$BSR+lKx@~>fU6Hq z^sc^$76~ifI0R)A%8SOlNPfW#=H8}c1F)WSu>g5jBo>!+N;*(hgMJlzc2c)z()aSH z-k3fZ-(rO%HrN_kbf5M}m7*RGWO=*NO`j@BR?mozjcBEq(FvE(_Sw#dqumYfU}h&49d+{_BBA z>_VaVqZGipmw2imv+$J>{mm=s_FO8w_lg8KdLaToXEeQGEcuX+-z~g@xbS(_5b}=y z*3RrxI(GjDho3*nRz<{!v!w2K|oz&OAzDsA8%+&CLTKA+|Y?T&PvxOl`noG^TBlNRm3+?m0= zyxiRp5c0Y;;L~Ja0^B(pY6Lyx}tS_&6&oP=qxHv(uw-M=G87odU-{-LavB_QX}R zJ=~1+^xDdWWpwwLt6FGtS&}^;sQL=GAy{dm{mDquzOFaNe$2^z-YknITiPtl9sx8q zV2Q%XYj>Jo`H3%@cea$LOKcO|rZbnSq?+TJ8!IGTF%MlMjg0IjeO$@P-KC?kMh5nn z$rc~`Y)3MsbRDC1?lAc~0_o5g7aAMv@`o6$M-G%M$zBSQ02465j|Ks^8mzUyOojh0 zAYp#NR##8WuB;@TQZfjXp|;-G(vdacJxC_V~bd+zf39POD)j56KkUY0pmZ zoLJti&{YvI@V2UYJIE>+20*4u!@)_9MPCG)m(lrkFr_9klIZFwxBanrnE)iye@0)b zZ%a~)oB|CfTZe^^V_Y^q@l{k%?<^KUy3t>TnlHv?Qool_#J%`0McVxP!TV7pvwRko zfadtKYNe@UqB|*UrZkkf{*x(=@yJ|qL}Uo@M$U~Sw=frDBH&bY>(SMs51l{;bHsF~ zd94Hg=gf!pRKaf*r@`jXegISl#3);$L^RkUY4a;9JAO!@xwB?cQi&2@sZ6kr7+pw= zH1kO@{dg23`|qF$6~);_;O0t`5#=UInql9HVJ^@~2dcem9MBC3*RIsu!@&-O_!cbCu6ZpXt&NIqDZf zu0rHVcR*dZom%4s_9=(SiWmHm)Z0bQjBnK2jDP)8?v6oe9vSS_ZR%>o_`9QMcRAGJi!qD=Ob=O0)d&uh zkiQsG5FfFY9&BI|PIJIuS?~ZJDCNH4V+qhM{Wq<2cJlL8k4E0&C#1e@lgQZkCnhw1 z12yS6jmQ>5F#$?A81%aFL{dZ&695rdgJ{gGL#ID zV$RuJrOweuBo@LrP5tH@ihg2Ji1$G>eFln(D$R%e+=(a{AH*R7`Hzs`T!>F$ z%3?1EGKo(CVd(Y08Sr|xjtC|y(>NQmOSF9ZVws%@FtO{kL5(h`NR z`>#1Hk2*9qJ5N&e+)=FK8gV&B&oejcyJOiZ) z27|x51S6*;mKlw^{&z_7Nr)OAvLV@@BNle!0f7_I&Zl%;cL}c6Y)zF0SaZ3Arde1{ ztL!EG_Iq|`8iJ-!o^ocaNlUzO&?;TVrmefW!jj`W$=9L<1$mU3n;=RV0J$(qx>^^^C&Dx^T*Ct14* zEw84!2jY>%;9knHab|$WNgX7r&C`4@3~Wd-nlVynLwJb0B#=4^##A;U%fwh~m>zv9TimHoF3CSGT2x+s7|0j7NH9v0|=cST7yS za_8Ji{j$qB#smx0O>vqsit%MxoAQ+ei#!i8K5mX1>52Puvo@S25*ZGOXL%je@%et!d?<7bq+AiJ=to;tgG&2AudK@8et5=zjNUOmkw3v^*8mIVost4DFZO|p zdBc*rTRCdYJT;a;%c4Dr)!w($sOcX}kXQn-Kb|L@2uBWqGSsJgAfs1|32&1mY7*Ks zxEPj8NQjJ!r{O>7*3q6cS)(Molcx>gb#A%ZqZs8aQ@D?_DmrP5$5~~slvYPB%MFq_ z^((29HBZg(e_D&O$^iIJ%}>lX2E}>_Vw?C0H*E}xYalllkBp1@cRyzew2IxK zxusLfN6&Lg2Hf)Uiu9Y*>HavU%;hi%T0!aCp=k3V!}CGK`G5gki~2vr>0eA(e=U%z z6qbVW*(K(PjQPVJT*9-?kXaa){#>YCe_<4!@i<0tx`$3W_TmrRlgWM~vCWsAm-Rn@ z)D-1G+yS-kAXesuMs@Z7usk<8KCrC+LHmn$n(HC1!1L};d-d9O-W0o4b#C+ggcT8L z5F)FB@UIP#8v~pu`KCyIsSd-)*S(P7KJ$mK6@Mg{6q#(peNcul% z!hc)>#Ta>kp{vrF|oy&cb%s0G}ln;;j1x1X94ptm#Sq>1#VK5*k?E`6;LdMM#vGsH`NA=6Csq74&ZxW@BCo$Cq$faN>l zTUPx|;XtATf6vhLlYxyd4`J5;joAGj!gpM8;=Ew3HXRhk_=Ggf3p0#+sviM=&igIK zj9s34TZ(B((8?G4*euA%ZS`in8jE>R;;P*PRS$r@Rxi*K{d^M)mAF9f-^-#$=F=Qw zVb9Sd9Lp9gE*O+CfP-tx&$LR&*;P>TvdJogs8rDH{q*~7gd*h2tf8|r-S_*|jH*l0 zW`G2)@r5WG7$n?W%r@Vk#K1+=HlI-lc;XB9eC>jR$e8Q!FFa+2xtK0`{$4R-nbLL~ zpE95(km^reFSbbnAC@l0>X`CL-RQj^|J3UeMD|H9rf0=`{PW@~=2=Z#^) z!i=Z)d+c5-;j!ZvKcN_7FOhUF)=1jD8Pg4|k}tYJ)Zx{;IM|+QeRmln8~5T`i&?XS zrQ=v?!Z-d9smM?dX^{zx@!ziI9c>HsDLL0t+o^H6YK{8(t!Z>y0KJ97N~4(GKqL&X zUY2qXKeSBS8~mMQ3_{GadY)<{yaP@TZ+guoS!UaGxBW(_zGD4wOPwX|#_Yi&zg;z> ziXD3+YvXPS>$JR96s8X=twdO8qmy4! zoGw0a)}pOWo3uQaq;2k2X<(tQY|dQYvP>2a?{?M_OUx4$~c9Cla`7xim3X9B!ChkU|x}9I$5SuZ`@z)~?PDj)1;Ko=NLsGvKdZDgp)uX@KO(0Fj(E zIPjp6bvk?_c}ILNiWHbw%WH`0AyrwBY&x6)1ayerVS_Gk$>;pR*ydKT7~Z`LKY}JaPaa&ikp?G-H$^* zg|xNIFf$Iblwc67xu}X&V$7YVzIw{EG_3gKme4!Vr&aJ$>ufC|6Xq<#di%yD%#4l! zUfN3(GP)}24Kq7p-yPL0){jO4Ei#|k^_lT4PRI=MppNbWcLh|pN!}S>FbnGrWH^5+ z4h`Ntuzr*t3@WPq*3n0kXuih;sUjhYer6q`1llv$!CxFI4OGi}*)iMH`O$(a3oe~g4$aDJ;8j-?p-5^mh|T=zYPxXA zZ`?`mn8jhgLwpVs(0!wZBN#Imq^7o2ydzhXWNG*+WW*SfePZJ(vTt7hEaFxIzL@Q4 z63VuxrIG@GD_Z18hM}xxWfX)Si|wWA%oH7m+0P2Q53Io7(%aNc=9^Roxcn=2k}^{k z>!>d~n)Qv;N3Q&x)E>DR(JmT%$=e`$`?BGnIf(=>s;P~1oW9avhteq;Zg|ae@B}s> zH<^rJI?n?2xtpsDd4P5C-)pM?&V&~CWk(?2{k22RaE9?!#I@G($KLC0`lBx?y)PHK zaqp`$x@EpsYT*-RQ+MgxZL`y~YZ+dsF8Y!=a|13D54^PyOnJKfky=zHP371!!>2Pu ztWlc{*wYSKLhtpY9*|=n$H3b6zOgd843@D7A1L(IAkk^2xKKQ*6T_?k5)Z=-y30}H zh4~=OU+jztorsrk82ohJr~#LkOmQZ-AJ!WvPOzqO=QWry%b9$@smmUz zdem*~=v-XqoZjZ#-j3t|dRVr5)NXe(W_K?|EcG#+c?Gs!y5)Ec2r`AHhTi56n~qb4 z7G64F%f5t37wczHTh?=t+NBRGLg+|a9DcCA6ftaOEuaj6d%176ozY<9>f zRVx}4=`u1$OYL;oS`~_}{9V@!w}|Pm9JenvC~{jORGRA`Fd|D_IkR5CAgV)wFU?jE zew|}$+c!)`7O&5I`XH+qe96uC0XyptE#c$_=j_8?L-N7_tR4Aa)!$$p$ zi4SzRqIQ`;y1nvaUiC-&rVxp_saz7dbVQ0S7eogsMq4V-g3-ZHRn^AmDlL3Xq8(As z%qvkrj}Vq=Wm>uxCj_jpV^}>9OQthg(g41`UUtX4vnZ3{IvRc}@D=;89>Xqb11+8b zV|y15YcK!++fxhw=_bk(Y(!<0RI0cPOGHc8W}POcbSu5ZrodHh<+Tq}!*<+=r&I+{ z3Et@>!|2oB*iF5W&N_JlI^&tDHr!!3g#cxsuJ0D_?XB z25Pt?2+CzPd4Ll*^eBp?QxzXiq;~X z`QIA9irZ2jV+y5V6kgCK*W|tuStA;t;kj4$lpoF4yU;UpO4kjp*hU)SRp@tX6i*KA z_%X9&aI*MfU}{i4HTc2*yAh2YOui@i*M5dzBsGXI1E4)o$I!nar}VvgAV_g-h3e6N z?Ss-u3ZvKx8{0zK2HV-zU&W`^mW-&!H^(*~N+y+Rc$axBomytHTP<~w*_mOy9*a5@G(yc)EfCh=EtK zL-0^d+?Pl9IDm(_RlgT!yiK@rP;S6+PD!>-H23!9<+o3fL_>1pIyf9SLWyHhmC8LonDKh)C_JIa1;& zI%4I!9u+svLiloM0>jS$@E5k{r{N$>5{&bHkb-`uW+c$Kx2l+;cYcq?C~j_H@__xcyc%a3J`nd9ot6 zxWgp3G{-`FdQU?kGQf(Vjv8|FTah82CozdUvJ83K$|{JT@OGUylonA3z!S4@p=V29 zCZr?t<}O>9N1Hmf@!@-z|9Q3yeDpf}0>D(;a;ehv%;+h|BiF5(=?Kn{Af8JFlpu&L z!|CJ4U~lVjC$V-|iNz%AA}c1ATD^!%=`a;hsz3m$>62OTq9A;xJ#`gCiyMd7;R9%f z%nz{wjsxsQAjK6*=}7srE!2SKlX%48MzR|UT|;1HvJ{H<@g{7P#p+oTiVQX*0i%Ji zSIP>IS41H@CZi=)jpi3TM!`#|QWaS{+f2Rb(wj|pqU+-mT2&GxBa_gqEp+TphZ)c5 zm-SAfR&*o|^zMs~)vO`B*4onJU=6FBQP4v$1b7#Od>Qr6s9ph5M=?wUCwIPdrcxy_ zjkcrFrjw=RQx8^A;f!;fT*81+xdIaFU=u^dlhg2z|y_TSD;Zn z(w5Kj@-x4m_z>emaHzq-8qT3-Z0OZMQs00B`j`CB-2-HR8iUXdW0f8qMg68((;9DX zc!qcI#rZYf+CAJh75&@TNa+0LrTa%y;bp;d@yqK#K8OC-RkWd)(WNUN74gqg>ndSn4aS z!;e2jy#Pu$oIH|hDOe(zwon)~@}8zp8MU&!uxB<=m-R*1GrAa6oz?!BgSulLO{pfL zL|JtX*}6Q!tWez0A}&`lU7g5y9>r-?kO*%{r2-p=&2d;3qVW&{pxFm1k!a3~WL}lA ztRilB^tn75E2hN7>Xn1s%G#j&PBx}3w=CuUouA{Ll_bB5o+(j{M0>F!#A28P)<_nh zE3FdjN+QhQW*?%4`cf!_>fa!}uWW1zD?((X5h`ds3MCOI8Y4-R$qDKoMxt(W2ol|t z+PMtkT{|ZGv~3IvII&GUStCx-D+(8vV~qf6QqV0AZ^_gQ!$n`9Q#nefTWLu5`Z7GU z3Rb2?ktp_#7cOzU{vZxVz%vEI~*oL0o>w$}cqbe1hTks9U5H1pQlc1skh zT#%`l`$_H4sfoAdId`?>)v?$1wC$(s&8}mV!d2CaupM^-B&;~p8`Ho8^v;wnU==l= zI<=!tA4?NkJI(l>TF{o1H<(%M(A|rTtUKltR&%>9RM067J)IEUE0=%U)V0Ac$$g57 z>VIHtJx_Z?#n6}BI`%!%GxoAL)|8FJ$oykMJUVU|DtFi;jn^L)TSV*^4kiaLC~JM} zUv^KMZl^;5wDV0f;EnBoTciBpW3l2?WY60zO{=f0wu|0m;&Kr;m@1$s3RsTca9@Rc{IBN!tc*mMR~)v%x<0G$CqlGVB(4h$t(lRZn*q z37!vLr&gi%q5~_D&iui4{+cn%I`ojSoZ7V&Z+PhRa;Kc#joZT<5eAFx?WVz# z>Ktr$P0gj)E^%pwo*MhJSx4koPL1kNj5WWB8ta1W++&@G>Gi0PI=6u$%0VU-jIY)v z$QxsT1e!;6;-u_Zy~#;uTGhsR5`JxA9xTt^9(y(+c^*5f<}2`>`@?q6IWQLUOwk3l zZaDoqup#sp)&Lb4!g`bREF#AA$&WhJhx`6JS}oWI`d4b(j4-cZlB{EOJtnRbyu=J~ zKe6tV19@&_Swdu(Sg;|@+HKZQqT(OC$yX=9J=|=OuQ3LC4nZF!sM+=%H2`Zjg4y8Y zJb0`u)?6_qfSo_iTABWa?k!EG)68E%2ZAjJ-9>Y^;vKzKeR@bDaTD&6S@Ym*c2d4> zugeSJC5u3G2=6N=<$JQ#t2#4%>=Jqp+(nZmYSfEW?!Bi8YWi@{tD1bU6D||ebt?pb z6MMBU#L}V_SF#@N-E0W2?F7B)1kPT7i4V0<{HGy>u`0?5ZR?$^1CC?gd0R$ArVG!> zbS+yyWN&mSiHh{Q=9lN7$TRhKMk$|2vk}_b!s?%k{GxE7 zgZ7-XZmG=|h-PBoBuf_d&VK#Jqb4fARr@dG)cdZ4*UYic2=={DEiS8qibk0ma2eQ3 z#uCo{l=2a^_22e7DJckO%BJvtG=|m1QsySv7(P|VXHbIr&t3Gq z>W4>k-LWoY&5MD`kiM-OTpR&I$Z<$~xIK4-zk-$2nsRHf6kKIY%faoNZ?@!MATZar zvZ!`p6@$Xf^}s1snBa0Xt1M-ew^h^Zcv9G(dZTLOjm)PUXokN?e1pC1ci&o5{Gndd+y%+ zJ-qH_2muCv;QMiOb3?eVkIV%)__m)RkOqMX5g-&JMfiE~eiK4^Is?)r;@%&q-r^dL zp*V0j{I*3w&xNNECz6VqkR9K$kPS%(a}D0Y@}%mZTgWkWx*BWIj-)?T2fYSg27GjJE=HZpL|lV162 z;{2+@>#O)OYo2&%Rnsehz?Kn~lHIEc$deMuWGoC-yF$ll!2sg3X^>YiCK!$oD5%IU z!<>%f-ZlLoYw;i!!8~|YHnC<+AHU5B(^_r#x00DtZJR=6hZc=N4w0B#szpT+q=TFV zrzgw$MX4~SM_TD542^ojh9a`l*uZ5nAE`N5qF$4fGWN46{5*5VPrsO1SqOG_JY>ba z%CvU8x0kb%DIJL;3M(raF9xO))nMQE*AN<-3@ z=DumJpEc=-jOCulj1{imtCgA|-ste#!429Y+}*NJDjbSVI9cEra*fxegKS4z%ALqb zt&tk{m62=r+-4O6+_i^rb883i=njX~k!|f>S!Q?pdH`St5#c6$^t*#<#Jj3@Ai~Aa z4pn4(_@J0~NBI$ncg+#X_vNrTRo_@fRo{p@Ro@r~-*@#9FW<8vaP++=1OXyp$|^aW zMI)q2%*bLhMAYrL_Xy$UJ>C*Zldknk-s)|Dn#Eh;LErGTw%`8%2G?`NXZ;AN48T$}tsJB&ZgrO|D0! zj78jN^ES!DLbnQTd3ogDE%`}DlLj80iFBIF)(rqGv7w) zlzJ!w{;D$H#bdS%ug+&;YJ&4I!OTTjzvV)?EMRe@J+pRn+|IyDY&$(#%_{mz;`F55 zhx1|_n8dt<3N-~)<`pSeL{J&mk}h2N6>AQyK?)jHGl2dNX*Y4`cunFl_I`~ z?t@mVhY1hHM0u`ZlI8g`nQ(&36vfG@k5|ZYhw<0ve`C4cIOm`t z=QO7=MaIi|TK)+9C%kBLr{tuZ+@#-M&|kBktQgPw!e*=G#>5rl%2FoCTE$L~cL0u9 zn$#785o5$ly?h=Y7unH<;w04-kgQ~y6Pi?*XhYJtROqkRMY&z-ceY@I6Y@ibkZTCF zU8n68pYJ?e6}mh3jO6xK^r@YSGDqsHb~@{_;L!Iyk%En4)A_8kAvfw7hJ8>zpUL9KI70-hN@&%;5 z&EZNKgwKa&v$s4lhQ2#Lyx z_xjQ=akg0IR?OT-*IU{16e;I$*@@uMZdSntEEEFPlDPRX#60Tu(ln%K*p9mOQYsGd zjp~@Ze05!Uw|Gh0|9I)3oMzD^g#}fKUaAZT3a?M=kno{uXZWhR#wZO2-r(2!)g%fP zJVqn=jx$%hsSGUgKDrmS!~i7U?TjQa0BP6WG%+(>U9?Yl4lAbF*~g$eiEUDx6px$x zAjyCSBEjFT4ru2xYl15o?mmxNH~ai zA)`o&6> zL$6iMh-uK=M`UN#j*W@#T=0&jB&|tzIP?*v`HWp$6FAPLcc(XUW2mxgA8H=mSFTPht|R;X=Gr zSA;2?M{b7w!pJ}X0=gJJhAlpMHyNbFt&^K4RaJZOVPh=2?gH|N8h+l;S;;3XPH*{< zOglvwKN8I6mp5f%Bg7Dy;XI zbWase!(-7vDoVpICCOe$dNr#u%;KVGqqNb9`Y356Z5=u)+enO0Xwh8}9O$*~OuZLB zGgnzreR@5w@D+Nusx)UfgGy^C(rH5(%69DOR(k>9?^ss}=5;5clUKN^eTfSFF<(zU zfgWRqoi4jZYvsxVb+nQ*?IB`vJ-RLSk zm{+9HbxnO%;&thXHS}qB)m-=BPr?0b8*aO~XA6Nl8lC>})8nSTI{d8w*mdf;=pH*N}>0$+#>*Ln^-vx@ZLJmI>falB}`seD@}jzO&uW8 zd|U48eNFUhWs`zE@`d_=sD5qq8#qq)8#sxK$bsG+I6&z4)2k1MM{YsTXsS7l_3t^Y z_2a0+SIcPn5{NUXX)#q+(QMk`P8sVJZr+BUG!?qEirId4KjS${Ah!2JfZj6w^z{LZ zCP@|R5?Ej{H63VnIqre^iw{68i9F?+D>}XJf;jP1=C$H7eT-U)8`N zvsjx--FDMF6%958PCXKDH@&u2Tii4}DMt#m81BhlSY36;Kp2U%Ayfu{S zeRRU<@yoh8LU%&frz6uQubbWU3w>`wON;jlLf>{bYq+z*H$d?F{6bOAM*pb9)Yyjx zvoqNvEme|fG+V3VgB4a`i?ViIAM-gEGh5P}0JME2z$h*3qUL{zNj{!>f@l3S$pHgfP;hWN;;yb-=9@tjm8H2^c6)!`j+;S=s?Q3xc<5TUoRpB1=;F+FFlwG?A;6^@BG)dYqy$4P)V95vYcSsa@^h40pQ7-=v)j_REI zB6gYZpS5WA^L$)0zQ{o*(3YDdY+D&e6Ec z+;5E233RV4esQp6@*;z_%`>B4nfUP&`XnE@+6s)B{&E-O7j6i8M2^KzL}W{~T-%iZ zl!mAWxKePY&jueBxKiLDma$mt%2_lQxR&zqP6xEa_XY*vvbF#D`pp8zD+9H7Zn)T$ z{>oqK8!v_UXFpI+TcF_*Hte4I`xx8iq*vjOMRkxq|Bx2!jSA1)*B+IpO++m6QAH@HzwEHHM%}PK(Knh9M2K)rCGjl3X z^7Q{a_lmz*8=09JnOQX)-Qf|Gelh=IJxG!;q=skyXZ9y~clVwAw+TDR&yWznr_f_R zM1-{Kl|pmgzn&aWqEkX6Q&qcyJ&TY43|qf4R<~5#bMt2{Mo06@D~c37%bA3IH#`oh zf<3w_g$NDH6C*#LMZpozg==(Pzo%qC4yXBY0h*~m2_Kp?=5a9JwP%?V4&qO&p0gb^ zsg^?5zKc*F(wv!jK`*856*-3%&w+A;p3|5D4coCP_!?fXV59C_+r5tD)*JBmbF2(P znSS2?DeEerqUyRf-5{WJgCN}{Axbw&H`0wFAp$aV_t0Gu(#+7^CCtzu-5?!GNc%-6jsZk0#slegn;*l;x;mm%7^uQ3)UEcBhF8Ore?MG7)tXS zgnNqGYU?kBWg82BCnrW^b=wPjpb2^m2Pb+b_hf(^bYM4vdN0!DOo@0;ZG03$#JLNy zd%tchG8|koHW1#A0Qf!%6Qt}>hB$!^eYd*Jj~V8x#}ND z8lV+-nTg3IP`*O^Z#@+R1bLYaf+;SBG*$b6{Vl7Hz?nD=f((=za{EH3K{8wtXaO}z z7~+%Oz6<%gW}H*5=gLO$r=O6R`gY?CRw_QHO6b}9g6Z~SHL#)fCn^RLWr@WC!8FMI znxvCjJL=?*pj3?>?m4UhSvI?Un(D0k-+$ih4`BgN30?tOEE+c?znuHqqv!aAQJIkx zUuI~i6k_b#?%ND$MBDHPz5xwLsl{nle=T4ukY`)ZDYV0FYQv$Jt#o+XLZKjCy`bX3 zvq#$bO}b!M(!9AWVSv|VQ?5~;pNPI*yt>gYG5;GMXs)iS|E)RlOdHW`mIx_15AkL< zTgf;ed-tm0G*0nsJb@;*uvY$r`1zb`9k#cDbS0x`hQI)y6It0z>ImP#XVjlN{$gZV z9%qKPVoN_F>rKj&f4ceE`_Z=vJ55;ZrGTt5YP#%|YwD@+DPHloaZM$h zjFfg;501=SDj-La5E@=Wr=V&Ja=F4HWA-Xr^%E>#B|4|?bsB5l3}7T#cSk8GV@-Jh z*tn+5KZU+^VKk_mcOGd{%F!I9S+b6J4Z!!&@JLX8`M9toE9Ibz3YK|lVAYfrfj2gW-}n1VMe%x;Ib_2W z_jyc|<@fa3Oe723Mmu2^l+Pmg?}a(80q>rr8ey8cOq4tk*wou;`hMetX9Q*`Ge$r4 zK;%S2@bE)n@JU3X2=WW}ed}7SEb!flGo&l5JLE&M(*PMooFf&$g5d&DFx_6eKPc98HpqEb6N~_WmO#^R;<50 zBvmWb*9hG^lVS|EdlR_z^(EbF+H7=N_+vqI*=?>g zTA2c-9sOotFIkC_>WcFcFwHSSF5TJW-KmP~a1f{~Z=tJY<{}uEkLCvc_hL*9LVST3 zHR$J6!Syr^S3#5R(xSeper8L=lm6Y*$Z!w;C}|{v*nuGbH8m2^Lw>OS3Lt~5?jw7m zAa$ZT*qVrPvF_w-4*Rqf6tIYOOGg@bu0>DR_^cO)ga+{G!ztVdC0!3H*N|GwksxSb z3HJ=SDrHHHlNc|VpV*U>C$?RX4YDjpJF>OOlo6DwO`r?H*38WALf-+o2R_ST7M-CJ zc>iAcwfyNeu8c*xv=Xz(0KmD&+(ZtN6(jj5Og~E)*{N;`cuA-g^Mf;eermk&^UsBO z9-ujdvLV!J?R!r;AfTMDP9DE>tL(*wYj7Qkb`eb#wYcRrI0`+)dj;hSnE~INtR?YD z-Snu&%9<+TZ4l#;)9PyZsDk!ZFlL9@!6UBv{q4EMid%k^jmLg?GXPr|Nz3`J<<>R+ zSr!_+;Uf(FED^|4j{c`(8J+q5B3=z{$z9!OZ@dU07mDljPA}ELhm#AR%Zfhcwn#jJ z?XL1xEakd=E6-t1!|i^J=5*k|v_$JsF|GcxIVNlPBQi-Y+Sr-)5$5z_$6)wYR9YGz zNuXdu^1DPNgO!G-n^Ka^x~I_KF%HWBT6!)b@$m&VCcK(N>%DHTIhirG#y8SrIB;O` z6Zywk`_fqs+lGe?Pwh+odpGHBwTFa4?^kVZJFOadSUDOb*fK1Aw`%u(qX%7tt0Ua4 zy^QDv8tV9Pa7E02S7;Idx~KX?{cX#)50(z>xU}#nI<#SC^xpW?c7eg6v2ZBuXu*kV z3?xb4{FBjf1DNGYbrZi5AnzD{sVQSb=L)TMDW9zQzUNZ;xg2t4Xus&OXS=h*a1Gd8 zNdBhC&hV=@%40a~B>lj1=hky`Z73>JSXt8Pe2984>oyURxqdu3#5B__t?B8`Dg4N(MJ z_4pJH0zG$5XHGKT1SFFtZmD;ZFR5A&uBoxJFt4dGVOgLrGGBZnd_K|e{6ZuV60PFX zWBCK@ziF*sI>wz*euNM~^aGq`>-=?HN*t`cN-va7r>XsQ>=QT0KhfGVL751xQ}+d3 z+h8}d3%8T2x9b2q%bHW<@aFNSQN5z0iwkfD2ihTX#LkylYJgrD&LL@xhrKsP++qZ2 z`PeqMu_qpp?7QZ=9DVn7(syWw-8ou7slZn>)%wWLS!rd;O&M7;(Jdr2RF@py&4l5TMMG7+ln5m|pKdHg+DCGn-p!@W$gq1c{3FQBPhnn0FzPfNt|Ds)_(-DVFRjvvec9m8Pqy-2Vg5^Ys`L2 zx6shyCNB!rv!C4m%4dPEDn-N<5jxYsexQ6r9RJGTovSayG_gZiN`ILkuYQbZ?w?X zcuw2zgn2cjHe^120h!e9~m)$E8t^9o_q>r5)hQS)_|&7D_v@Ed=-tc_@Q~oAFpeEu5X$ z?L0qRK8MEQ5W+{iV#S}?nzW*?TngWf>F(_0XjbFg%<-Q=c^1emsx~_lf>sN{2zl34 zh3l@!x?s|+CTA8Es_k8?xgI($`zlboxC1=~Fb$keH41v?5wT9tFHbQBCHuipPBcd?L5{q@m+GtmJ7OF^e#}5p#r3($%ib!YU z05jUX&^ghiQ2%&#vn#zuUA^vStEl#1{h>bBQq>n`nFokiM2R6LgL8bb{l?5ci+F_6 zLmOu1?C9~wHit5XcyrCHa2ciGqia2WwGALw9fvQMkgJ39v{|1JUJOS)myZkRv=l6? zLt{m0Hjv$ls4R2iu@P)Ps`S(ThL3_W50Lvv;_SW8xrOOm{K_a#IEf+8t$bhJ)s@~@ zZE&}u7Aj~AJ$r5qrP=QYqKt;j!*#(ULwFwVy~cmm5*AK+!0SeIz`sS~9#`a62z-lc zX;#rQk76{e6^!e#21+b)?|TfX&WoSO966WwD_{9cYGi-N*R;3qXl~gz||d#X(kzCxCTf)A&@&cp5sMXkHDgvfy4*N52F7>|Si; z>Gye3ACj}DUcE3ooWG*?JHV5e8wAvncv3CCJ4Ey>Xn!<1k0RD(GgMOun2cIWL$H%f0)ZTlp>-YQoxgEE}ry?q!I7apaWE3$D62VD|U_P%`TT!_$ z`WYc`V0#NB7Y8lW;caK^$f`ljX19&-u5T;vbEd znn@_7lSK5yc3UVYII^0ShExMa#$`cDK-rWdv!k;2Rn2Y|>)&)x<`rwLuCV=D`#Ef{ zLe09o@*_Iew8_Jy8^TgeNAgG_#i7HlJQyx3%Nr{ju5KkHGzl*~2MoW?a63&tLn+zw zj4CZR!;CC^6Lk7RtOg7d$z?M2FkL{Jd==~-roPG{-m^2n()tBfOAOS0U zK-ckpW&S$E$9lh@RU8{NROMKN>z&(sp{;6@x{I{m7VYXs*86gu?Z0==B9DP%jd_L} zpT^Qgoup^AF38=)lnNmoYPbLR^?bnbwmS zmr){um7XU^kla(h_=5`s_)OeA44vxEK4^3rcL?zN@^a{jYuS1ml=UZf*-n6;41C+3 zMNa$oboD6yLhR-@KTQ?oiu8O8pvJE6;>p}ak{h@PJ8G!6rJsr1he(jt46EmC}~F0vpeY7nXh;R?51jy05QWBCT2ewcsz); zZqf>{*~slHT?gkEG2=BuqqG`|^I2F`b^5_`LwuR82cDbn<@KorOk0cCYrOHa7M?K; z<`t^+jGmp~YmLa%=*YzhAg=7VT0f~os=2~r`JP~>WsHHi)|-P}X*J-39LoKr2O|bD z#y~&4?NGT%I8E|48(<^w2*&|QO;2QUqMnBJH=%{NpNOsO+z`3YNjloltAz3F!i}rx z5`O#(r1Mt|Tdx_f1v!OSdiP`!VrI~+$WHB8Mqt}=hNnX7$tFHwN`cn&x30x{$(Czv zX3b2KM{+MaIN*Pp?=pXVSea z2HN8>q@vi7K_Gxkh-*IovUXLBedjCSWp%)Z#AnRJ9Esvrl*qdJLr2&6UTicR?!;_6 z&y2?Kj?ZEz6?_T^{5%dPM)28kW1I=+!!nG4W1pSHVoZK|ciKGGM&IkIGQ{vrUl>=o zkm>g;8&O|Spo_$RbjXIpH}b)g$XAadUEY%A#?+n?2e1v8mwRLq^!XI`Gl^3T^iwFa zwx(CO^e<>(!KmG}eA5I;iLbThEB7SI_ABF4sh#qrTZvP~PyR)la2JeJc1_pn&~cUA z)PW3o6)FPmpQB98y!kDep(z8*b%{Ovac3if`i@OB@2Dayh5Fvfade%!q`Y1&#=3Cd z8uz<$>(dehR0c6nl3uxY9#~(-fUt zOVBt)f_ut(Q{eTh7oIEYonWl0HS^G&Z&djt68-d&4IgH%0z0W7WxLtP1o$eH1S&Fx z2#XQC0Lo?22!1s-1NqG{LS31}m=M6MHH(WHkAWPUGo4Ofg{GtqnW)0vDSp7VFgU^s zM#Lo8lDgw~JY0EMKO!t)bS@ z?S^FD7Og?m8C?guUXdBEATsM$?5vLJs-lq80f|$}K+ihf!*|hoz0qIB&J)LFAnhC1 zOn~4w>JUrw;)wSgS%5S{=a-h0(oa{bxjAZ>$lwsjYWIC{QF-1gD6CGxe8v+MNHpjhivs zRRz3P3zAo(7W@a?lYmGqXUSuMiUCAMj_h~7Jqo$jC|4)2=Woo=#BG9bwrsfpOUiDV zfb`TYjevA`$s5~4FT?L@zxv1qg5QnS+J(7k`cXmS7WWIx5pFEuU)aJI52kwix--9)2>2Pxa#2)HEBS6 zsS2D=oa;&wGZ04_yk)D;yi!Op>WmesCj4fMQ@A-r9S=`KWW;?s0!_iBjW7NAI(}&k zXf-{G(X1mxTzJiycQz70nVlKo9qk9mdE-pDeT9!D-_Sqmls6XY>8mo)Yh_UVXqiTV zowO;Br`fDoI~%t6tlONZv0!lgYI&)ogpu{;2mddqG$Xiz_6vleP!$O}tM67+8pjdV z)W7o{Oq5ZFXI!_w?Tls^*@hpDPU|BfyR0fJK!o0eVr)!npDR0NAgk72^_vtROPDEbP;6K;LZ^zx2=bUb(} zafug8-31wqqVWmZIG&IGrFkq4BHDV!JoMChSkiXzVho%nhFJLBG{7fxRiP?+f(>Z47JX z*ACY#*JRF^*$3V)A_t6&DT)?QD@xnCKU_XK>_hyx>?Fgm#d2wsTb+F);k}D@8G%@} zR}pUYxv8mT4xp23 zVdHBR{dj{W)x}LA`b2aM+V#Qumk0dx)Y(`p1Cyvn5vWDnM}@&lrA)D)N~?dpRF|vtyQVqXX7901|1e(%sqi&X}l7cFE zc7OJseks%)!c`Isa6JXQCfD8^ zKM!qqZ}7N%SfY_ktHuGGm}?OE3j^!{-A9gsWHqZ^IHK;I{BY*`Zp-kZfKie3=wi0`ENj9>8)bNu$@8fkl;NM!^$Z&TXSXbGHe-luLd5eFkv4U!( z``l4@2?F9>QmUq{!YK*1z4aA#2=`A?iWx`>Ap|X!=lm53sM3eO7g-0r{42~<2l>mp zs#m+uiAmR>or%IuUJanb!3p2(l0^+61F=R3Kp9Oki1>dzqRgE%m4T(qq+pE-4yZIj z10-&H^8eC}U~c~ZPB+T6(cBjqcWC4Jr$jlhs7>geyV=HmPmr|p-4m+qqW1)_T?zf4 zs^q}!c5A9XSDU-qPG4aj%)%Nito`Q??BfP>?BKe?AQidK>%(I523G!;?LPq6f3S}U zu)Ra%UOjv#DFQkg(5IUKD2PT4)b7Hf{-ggv_38(*R5&m&IWVP@>Rv%JOu+!|or3*N zraR#WwS^xDUlZH`4~ZWGOa5B=w|pWT9Q6aR9>pDauZ!f~5=J*ELI~>}Pq>@m&eDTA z5)XuvxL`p3JJ7#x`@z96KLF8)+-ckGrn>|Fdy4yigBm9N*A-NtLeG;s@PmVT545f; z-hmH^v4Pn=Pw&7F4uL%Y?zb*!44Q;Bo;8aM+j>DBOp2&sXRpMX1sR zR)*67L;D`z^Ri*Q481$vQs1+C9%er&LKdt9?l2SZbwBN$BU^OX$1*S{6kwx`>wzQT z#xP-WU|B!i9q>W!_yLgC6nNCHaBtz|04ai`?VW|+&bYwcZ};6@Gnsg{471<>%Mpt` z5Vmjtwht)X$K;<>Gpw%l15lphoh!;7Sit(8dyQ#!`j7&c-}!$ITRs50f4nnaJVH&TRkZ{U2W$e+mG2|w6+e_*BF7v_@eKOOvc6aIg# zSO&m=v_PSuzgAukks{nC0*#v(fpJ45_m(SRypxo__XP!bHH3d>-Hl;I{Vi+;AjO1D zX%DPVro#q-=)VKNW0({nDhJjb_J0EOU|M}39RCGo=|A@iLW&Svco*IBsz?9yTVjN8 z;|DCVo-oTI4}`ahU|>4nB;@aqf`^hKu$BQSpfYzZvk_SNqQY);Bg}2?2ZFcdcY1-!diS;Z{}cDWNU+atHEbGZ!r_D!Nbkgj{q94}pAY{J>9S8J diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..72a999e4b 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:16 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 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 diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..347aa08f3 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:28 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 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 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..9332fb8f6 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:46 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` From ba76c2f843490e69bdca80bdae3b5fe743c2c3ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Oct 2015 17:41:18 +0100 Subject: [PATCH 0229/1059] Add a sample showing how to use Spring REST Docs with Slate Closes gh-124 --- build.gradle | 4 + .../build/SampleBuildConfigurer.groovy | 38 +- docs/src/docs/asciidoc/getting-started.adoc | 13 +- samples/rest-notes-slate/README.md | 24 + samples/rest-notes-slate/build.gradle | 64 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53638 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/rest-notes-slate/gradlew | 160 ++ samples/rest-notes-slate/gradlew.bat | 90 + samples/rest-notes-slate/slate/.gitignore | 22 + samples/rest-notes-slate/slate/CHANGELOG.md | 49 + samples/rest-notes-slate/slate/Gemfile | 12 + samples/rest-notes-slate/slate/Gemfile.lock | 140 ++ samples/rest-notes-slate/slate/LICENSE | 13 + samples/rest-notes-slate/slate/README.md | 128 ++ samples/rest-notes-slate/slate/Rakefile | 6 + samples/rest-notes-slate/slate/config.rb | 41 + .../slate/font-selection.json | 148 ++ .../slate/source/api-guide.md.erb | 206 ++ .../slate/source/fonts/slate.eot | Bin 0 -> 1876 bytes .../slate/source/fonts/slate.svg | 14 + .../slate/source/fonts/slate.ttf | Bin 0 -> 1720 bytes .../slate/source/fonts/slate.woff | Bin 0 -> 1796 bytes .../slate/source/fonts/slate.woff2 | Bin 0 -> 796 bytes .../slate/source/images/logo.png | Bin 0 -> 3507 bytes .../slate/source/images/navbar.png | Bin 0 -> 96 bytes .../slate/source/javascripts/all.js | 4 + .../slate/source/javascripts/all_nosearch.js | 3 + .../slate/source/javascripts/app/_lang.js | 162 ++ .../slate/source/javascripts/app/_search.js | 74 + .../slate/source/javascripts/app/_toc.js | 55 + .../slate/source/javascripts/lib/_energize.js | 169 ++ .../javascripts/lib/_imagesloaded.min.js | 7 + .../javascripts/lib/_jquery.highlight.js | 108 + .../source/javascripts/lib/_jquery.tocify.js | 1042 +++++++++ .../source/javascripts/lib/_jquery_ui.js | 566 +++++ .../slate/source/javascripts/lib/_lunr.js | 1910 +++++++++++++++++ .../slate/source/layouts/layout.erb | 102 + .../slate/source/stylesheets/_icon-font.scss | 38 + .../slate/source/stylesheets/_normalize.css | 427 ++++ .../slate/source/stylesheets/_syntax.scss.erb | 27 + .../slate/source/stylesheets/_variables.scss | 109 + .../slate/source/stylesheets/print.css.scss | 142 ++ .../slate/source/stylesheets/screen.css.scss | 620 ++++++ .../src/main/java/com/example/notes/Note.java | 75 + .../com/example/notes/NoteRepository.java | 23 + .../com/example/notes/RestNotesSlate.java | 33 + .../src/main/java/com/example/notes/Tag.java | 65 + .../java/com/example/notes/TagRepository.java | 23 + .../src/main/resources/application.properties | 1 + .../com/example/notes/ApiDocumentation.java | 343 +++ 51 files changed, 7289 insertions(+), 17 deletions(-) create mode 100644 samples/rest-notes-slate/README.md create mode 100644 samples/rest-notes-slate/build.gradle create mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/rest-notes-slate/gradlew create mode 100644 samples/rest-notes-slate/gradlew.bat create mode 100644 samples/rest-notes-slate/slate/.gitignore create mode 100644 samples/rest-notes-slate/slate/CHANGELOG.md create mode 100644 samples/rest-notes-slate/slate/Gemfile create mode 100644 samples/rest-notes-slate/slate/Gemfile.lock create mode 100644 samples/rest-notes-slate/slate/LICENSE create mode 100644 samples/rest-notes-slate/slate/README.md create mode 100644 samples/rest-notes-slate/slate/Rakefile create mode 100644 samples/rest-notes-slate/slate/config.rb create mode 100755 samples/rest-notes-slate/slate/font-selection.json create mode 100644 samples/rest-notes-slate/slate/source/api-guide.md.erb create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.eot create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.svg create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.ttf create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.woff create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.woff2 create mode 100644 samples/rest-notes-slate/slate/source/images/logo.png create mode 100644 samples/rest-notes-slate/slate/source/images/navbar.png create mode 100644 samples/rest-notes-slate/slate/source/javascripts/all.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_lang.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_search.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_toc.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js create mode 100644 samples/rest-notes-slate/slate/source/layouts/layout.erb create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_normalize.css create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_variables.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/print.css.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Note.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java create mode 100644 samples/rest-notes-slate/src/main/resources/application.properties create mode 100644 samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java diff --git a/build.gradle b/build.gradle index 2439c1ccd..67a98120c 100644 --- a/build.gradle +++ b/build.gradle @@ -216,6 +216,10 @@ task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api', ':buildSamples' from(file('samples/rest-notes-spring-hateoas/build/asciidoc/html5')) { into 'samples/restful-notes' } + + from(file('samples/rest-notes-slate/build/docs')) { + into 'samples/slate' + } } configurations { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 5ddd17ab1..c960ee1ea 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -29,6 +29,8 @@ public class SampleBuildConfigurer { private String workingDir + private boolean verifyIncludes = true + SampleBuildConfigurer(String name) { this.name = name } @@ -39,24 +41,34 @@ public class SampleBuildConfigurer { Task createTask(Project project, Object... dependencies) { File sampleDir = new File(this.workingDir).absoluteFile - Task verifyIncludes + Task sampleBuild = project.tasks.create name + sampleBuild.description = "Builds the ${name} sample" + sampleBuild.group = "Build" if (new File(sampleDir, 'build.gradle').isFile()) { Task gradleBuild = createGradleBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) - verifyIncludes.dependsOn gradleBuild + if (verifyIncludes) { + Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) + verifyIncludesTask.dependsOn gradleBuild + sampleBuild.dependsOn verifyIncludesTask + } + else { + sampleBuild.dependsOn gradleBuild + } } else if (new File(sampleDir, 'pom.xml').isFile()) { Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) - verifyIncludes.dependsOn(mavenBuild) + if (verifyIncludes) { + Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) + verifyIncludesTask.dependsOn(mavenBuild) + sampleBuild.dependsOn verifyIncludesTask + } + else { + sampleBuild.dependsOn mavenBuild + } } else { throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") } - Task sampleBuild = project.tasks.create name - sampleBuild.description = "Builds the ${name} sample" - sampleBuild.group = "Build" - sampleBuild.dependsOn verifyIncludes return sampleBuild } @@ -108,9 +120,9 @@ public class SampleBuildConfigurer { } private Task createVerifyIncludes(Project project, File buildDir) { - Task verifyIncludes = project.tasks.create("${name}VerifyIncludes") - verifyIncludes.description = "Verifies the includes in the ${name} sample" - verifyIncludes << { + Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") + verifyIncludesTask.description = "Verifies the includes in the ${name} sample" + verifyIncludesTask << { Map unprocessedIncludes = [:] buildDir.eachFileRecurse { file -> if (file.name.endsWith('.html')) { @@ -132,6 +144,6 @@ public class SampleBuildConfigurer { throw new GradleException(message.toString()) } } - return verifyIncludes + return verifyIncludesTask } } \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 485dec89a..67d7dc178 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -16,21 +16,26 @@ If you want to jump straight in, a number of sample applications are available: | {samples}/rest-assured[REST Assured] | Gradle -| Demonstrates the use of Spring REST Docs with REST Assured. +| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. + +| {samples}/rest-notes-slate[Slate] +| Gradle +| Demonstrates the use of Spring REST Docs with Markdown and + http://github.com/tripit/slate[Slate]. | {samples}/rest-notes-spring-data-rest[Spring Data REST] | Maven | Demonstrates the creation of a getting started guide and an API guide for a service - implemented using Spring Data REST. + implemented using http://projects.spring.io/spring-data-rest/[Spring Data REST]. | {samples}/rest-notes-spring-hateoas[Spring HATEOAS] | Gradle | Demonstrates the creation of a getting started guide and an API guide for a service - implemented using Spring HATEOAS. + implemented using http://projects.spring.io/spring-hateoas/[Spring HATEOAS]. | {samples}/testng[TestNG] | Gradle -| Demonstrates the use of Spring REST Docs with TestNG. +| Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. |=== diff --git a/samples/rest-notes-slate/README.md b/samples/rest-notes-slate/README.md new file mode 100644 index 000000000..bb4de692c --- /dev/null +++ b/samples/rest-notes-slate/README.md @@ -0,0 +1,24 @@ +# REST Notes Slate + +This sample shows how to document a RESTful API using Spring REST Docs and [Slate][1]. +The sample can be built with Gradle, but requires Ruby and the `bundler` gem to +be installed. + +## Quickstart + +``` +./gradlew build +open build/docs/api-guide.html +``` + +## Details + +The bulk of the documentation is written in Markdown in the file named +[slate/api-guide.md.erb][2]. When the documentation is built, snippets generated by +Spring REST Docs in the [ApiDocumentation][3] tests are incorporated into this +documentation by [ERB][4]. The combined Markdown document is then turned into HTML. + +[1]: https://github.com/tripit/slate +[2]: slate/api-guide.md.erb +[3]: src/test/java/com/example/notes/ApiDocumentation.java +[4]: http://ruby-doc.org/stdlib-2.2.3/libdoc/erb/rdoc/ERB.html \ No newline at end of file diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle new file mode 100644 index 000000000..79788c3d6 --- /dev/null +++ b/samples/rest-notes-slate/build.gradle @@ -0,0 +1,64 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-data-jpa' + compile 'org.springframework.boot:spring-boot-starter-data-rest' + + runtime 'com.h2database:h2' + runtime 'org.atteo:evo-inflector:1.2.1' + + testCompile 'com.jayway.jsonpath:json-path' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" +} + +test { + outputs.dir snippetsDir +} + +task(bundleInstall, type: Exec) { + workingDir file('slate') + executable 'bundle' + args 'install' +} + +task(slate, type: Exec) { + dependsOn 'bundleInstall', 'test' + workingDir file('slate') + executable 'bundle' + args 'exec', 'middleman', 'build' +} + +build { + dependsOn 'slate' +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 GIT binary patch literal 53638 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%0AB1q=av0)K=-*!|(RDoglJ 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/rest-notes-slate/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore new file mode 100644 index 000000000..f6fc8c00b --- /dev/null +++ b/samples/rest-notes-slate/slate/.gitignore @@ -0,0 +1,22 @@ +*.gem +*.rbc +.bundle +.config +coverage +InstalledFiles +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.DS_STORE +build/ +.cache + +# YARD artifacts +.yardoc +_yardoc +doc/ +.idea/ \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md new file mode 100644 index 000000000..ee496db6c --- /dev/null +++ b/samples/rest-notes-slate/slate/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## Version 1.2 + +*June 20, 2015* + +**Fixes:** + +- Remove crash on invalid languages +- Update Tocify to scroll to the highlighted header in the Table of Contents +- Fix variable leak and update search algorithms +- Update Python examples to be valid Python +- Update gems +- More misc. bugfixes of Javascript errors +- Add Dockerfile +- Remove unused gems +- Optimize images, fonts, and generated asset files +- Add chinese font support +- Remove RedCarpet header ID patch +- Update language tabs to not disturb existing query strings + +## Version 1.1 + +*July 27th, 2014* + +**Fixes:** + +- Finally, a fix for the redcarpet upgrade bug + +## Version 1.0 + +*July 2, 2014* + +[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed) + +**Features:** + +- Responsive designs for phones and tablets +- Started tagging versions + +**Fixes:** + +- Fixed 'unrecognized expression' error +- Fixed #undefined hash bug +- Fixed bug where the current language tab would be unselected +- Fixed bug where tocify wouldn't highlight the current section while searching +- Fixed bug where ids of header tags would have special characters that caused problems +- Updated layout so that pages with disabled search wouldn't load search.js +- Cleaned up Javascript diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile new file mode 100644 index 000000000..0933b9d68 --- /dev/null +++ b/samples/rest-notes-slate/slate/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +# Middleman +gem 'middleman', '~>3.3.10' +gem 'middleman-gh-pages', '~> 0.0.3' +gem 'middleman-syntax', '~> 2.0.0' +gem 'middleman-autoprefixer', '~> 2.4.4' +gem 'rouge', '~> 1.9.0' +gem 'redcarpet', '~> 3.3.2' + +gem 'rake', '~> 10.4.2' +gem 'therubyracer', '~> 0.12.1', platforms: :ruby diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock new file mode 100644 index 000000000..fff5ee10c --- /dev/null +++ b/samples/rest-notes-slate/slate/Gemfile.lock @@ -0,0 +1,140 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (4.1.11) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.1) + tzinfo (~> 1.1) + autoprefixer-rails (5.2.0.1) + execjs + json + celluloid (0.16.0) + timers (~> 4.0.0) + chunky_png (1.3.4) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.9.1.1) + compass (1.0.3) + chunky_png (~> 1.2) + compass-core (~> 1.0.2) + compass-import-once (~> 1.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + sass (>= 3.3.13, < 3.5) + compass-core (1.0.3) + multi_json (~> 1.0) + sass (>= 3.3.0, < 3.5) + compass-import-once (1.0.5) + sass (>= 3.2, < 3.5) + erubis (2.7.0) + execjs (2.5.2) + ffi (1.9.8) + haml (4.0.6) + tilt + hike (1.2.3) + hitimes (1.2.2) + hooks (0.4.0) + uber (~> 0.0.4) + i18n (0.7.0) + json (1.8.3) + kramdown (1.7.0) + libv8 (3.16.14.7) + listen (2.10.1) + celluloid (~> 0.16.0) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + middleman (3.3.12) + coffee-script (~> 2.2) + compass (>= 1.0.0, < 2.0.0) + compass-import-once (= 1.0.5) + execjs (~> 2.0) + haml (>= 4.0.5) + kramdown (~> 1.2) + middleman-core (= 3.3.12) + middleman-sprockets (>= 3.1.2) + sass (>= 3.4.0, < 4.0) + uglifier (~> 2.5) + middleman-autoprefixer (2.4.4) + autoprefixer-rails (~> 5.2.0) + middleman-core (>= 3.3.3) + middleman-core (3.3.12) + activesupport (~> 4.1.0) + bundler (~> 1.1) + erubis + hooks (~> 0.3) + i18n (~> 0.7.0) + listen (>= 2.7.9, < 3.0) + padrino-helpers (~> 0.12.3) + rack (>= 1.4.5, < 2.0) + rack-test (~> 0.6.2) + thor (>= 0.15.2, < 2.0) + tilt (~> 1.4.1, < 2.0) + middleman-gh-pages (0.0.3) + rake (> 0.9.3) + middleman-sprockets (3.4.2) + middleman-core (>= 3.3) + sprockets (~> 2.12.1) + sprockets-helpers (~> 1.1.0) + sprockets-sass (~> 1.3.0) + middleman-syntax (2.0.0) + middleman-core (~> 3.2) + rouge (~> 1.0) + minitest (5.7.0) + multi_json (1.11.1) + padrino-helpers (0.12.5) + i18n (~> 0.6, >= 0.6.7) + padrino-support (= 0.12.5) + tilt (~> 1.4.1) + padrino-support (0.12.5) + activesupport (>= 3.1) + rack (1.6.4) + rack-test (0.6.3) + rack (>= 1.0) + rake (10.4.2) + rb-fsevent (0.9.5) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + redcarpet (3.3.2) + ref (1.0.5) + rouge (1.9.0) + sass (3.4.14) + sprockets (2.12.3) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-helpers (1.1.0) + sprockets (~> 2.0) + sprockets-sass (1.3.1) + sprockets (~> 2.0) + tilt (~> 1.1) + therubyracer (0.12.2) + libv8 (~> 3.16.14.0) + ref + thor (0.19.1) + thread_safe (0.3.5) + tilt (1.4.1) + timers (4.0.1) + hitimes + tzinfo (1.2.2) + thread_safe (~> 0.1) + uber (0.0.13) + uglifier (2.7.1) + execjs (>= 0.3.0) + json (>= 1.8.0) + +PLATFORMS + ruby + +DEPENDENCIES + middleman (~> 3.3.10) + middleman-autoprefixer (~> 2.4.4) + middleman-gh-pages (~> 0.0.3) + middleman-syntax (~> 2.0.0) + rake (~> 10.4.2) + redcarpet (~> 3.3.2) + rouge (~> 1.9.0) + therubyracer (~> 0.12.1) diff --git a/samples/rest-notes-slate/slate/LICENSE b/samples/rest-notes-slate/slate/LICENSE new file mode 100644 index 000000000..5ceddf59f --- /dev/null +++ b/samples/rest-notes-slate/slate/LICENSE @@ -0,0 +1,13 @@ +Copyright 2008-2013 Concur Technologies, Inc. + +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. \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md new file mode 100644 index 000000000..2f042e7f2 --- /dev/null +++ b/samples/rest-notes-slate/slate/README.md @@ -0,0 +1,128 @@ +Slate +======== + +[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate) + +Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API. + +Screenshot of Example Documentation created with Slate + +*The example above was created with Slate. Check it out at [tripit.github.io/slate](http://tripit.github.io/slate).* + +Features +------------ + +* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print. + +* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. + +* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks! + +* **Write code samples in multiple languages** — if your API has bindings in multiple programming languages, you easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown! + +* **Out-of-the-box syntax highlighting** for [almost 60 languages](http://rouge.jayferd.us/demo), no configuration required. + +* **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. + +* **Let your users update your documentation for you** — by default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it's simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to, you're welcome to not use Github and host your docs elsewhere! + +Getting starting with Slate is super easy! Simply fork this repository, and then follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://tripit.github.io/slate). + + + +Getting Started with Slate +------------------------------ + +### Prerequisites + +You're going to need: + + - **Linux or OS X** — Windows may work, but is unsupported. + - **Ruby, version 1.9.3 or newer** + - **Bundler** — If Ruby is already installed, but the `bundle` command doesn't work, just run `gem install bundler` in a terminal. + +### Getting Set Up + + 1. Fork this repository on Github. + 2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` + 3. `cd slate` + 4. Install all dependencies: `bundle install` + 5. Start the test server: `bundle exec middleman server` + +Or use the included Dockerfile! (must install Docker first) + +```shell +docker build -t slate . +docker run -d -p 4567:4567 --name slate -v $(pwd)/source:/app/source slate +``` + +You can now see the docs at . Whoa! That was fast! + +*Note: if you're using the Docker setup on OSX, the docs will be +availalable at the output of `boot2docker ip` instead of `localhost:4567`.* + +Now that Slate is all set up your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/tripit/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/tripit/slate/wiki/Deploying-Slate). + +Examples of Slate in the Wild +--------------------------------- + +* [Travis-CI's API docs](http://docs.travis-ci.com/api/) +* [Mozilla's localForage docs](http://mozilla.github.io/localForage/) +* [Mozilla Recroom](http://mozilla.github.io/recroom/) +* [ChaiOne Gameplan API docs](http://chaione.github.io/gameplanb2b/#introduction) +* [Drcaban's Build a Quine tutorial](http://drcabana.github.io/build-a-quine/#introduction) +* [PricePlow API docs](https://www.priceplow.com/api/documentation) +* [Emerging Threats API docs](http://apidocs.emergingthreats.net/) +* [Appium docs](http://appium.io/slate/en/master) +* [Golazon Developer](http://developer.golazon.com) +* [Dwolla API docs](https://docs.dwolla.com/) +* [RozpisyZapasu API docs](http://www.rozpisyzapasu.cz/dev/api/) +* [Codestar Framework Docs](http://codestarframework.com/documentation/) +* [Buddycloud API](http://buddycloud.com/api) +* [Crafty Clicks API](https://craftyclicks.co.uk/api/) +* [Paracel API Reference](http://paracel.io/docs/api_reference.html) +* [Switch Payments Documentation](http://switchpayments.com/docs/) & [API](http://switchpayments.com/developers/) +* [Coinbase API Reference](https://developers.coinbase.com/api) +* [Whispir.io API](https://whispir.github.io/api) +* [NASA API](https://data.nasa.gov/developer/external/planetary/) +* [CardPay API](https://developers.cardpay.com/) +* [IBM Cloudant](https://docs-testb.cloudant.com/content-review/_design/couchapp/index.html) +* [Bitrix basis components](http://bbc.bitrix.expert/) +* [viagogo API Documentation](http://developer.viagogo.net/) +* [Fidor Bank API Documentation](http://docs.fidor.de/) +* [Market Prophit API Documentation](http://developer.marketprophit.com/) +* [OAuth.io API Documentation](http://docs.oauth.io/) +* [Aircall for Developers](http://developer.aircall.io/) +* [SupportKit API Docs](http://docs.supportkit.io/) +* [SocialRadar's LocationKit Docs](https://docs.locationkit.io/) +* [SafetyCulture API Documentation](https://developer.safetyculture.io/) +* [hosting.de API Documentation](https://www.hosting.de/docs/api/) + +(Feel free to add your site to this list in a pull request!) + +Need Help? Found a bug? +-------------------- + +Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. + + +Contributors +-------------------- + +Slate was built by [Robert Lord](https://lord.io) while at [TripIt](http://tripit.com). + +Thanks to the following people who have submitted major pull requests: + +- [@chrissrogers](https://github.com/chrissrogers) +- [@bootstraponline](https://github.com/bootstraponline) +- [@realityking](https://github.com/realityking) + +Also, thanks to [Sauce Labs](http://saucelabs.com) for helping sponsor the project. + +Special Thanks +-------------------- +- [Middleman](https://github.com/middleman/middleman) +- [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) +- [middleman-syntax](https://github.com/middleman/middleman-syntax) +- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages) +- [Font Awesome](http://fortawesome.github.io/Font-Awesome/) diff --git a/samples/rest-notes-slate/slate/Rakefile b/samples/rest-notes-slate/slate/Rakefile new file mode 100644 index 000000000..6a952e1e9 --- /dev/null +++ b/samples/rest-notes-slate/slate/Rakefile @@ -0,0 +1,6 @@ +require 'middleman-gh-pages' +require 'rake/clean' + +CLOBBER.include('build') + +task :default => [:build] diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb new file mode 100644 index 000000000..fdcb21f32 --- /dev/null +++ b/samples/rest-notes-slate/slate/config.rb @@ -0,0 +1,41 @@ +# Markdown +set :markdown_engine, :redcarpet +set :markdown, + fenced_code_blocks: true, + smartypants: true, + disable_indented_code_blocks: true, + prettify: true, + tables: true, + with_toc_data: true, + no_intra_emphasis: true + +# Assets +set :css_dir, 'stylesheets' +set :js_dir, 'javascripts' +set :images_dir, 'images' +set :fonts_dir, 'fonts' + +# Activate the syntax highlighter +activate :syntax + +activate :autoprefixer do |config| + config.browsers = ['last 2 version', 'Firefox ESR'] + config.cascade = false + config.inline = true +end + +# Github pages require relative links +activate :relative_assets +set :relative_links, true + +# Build Configuration + +set :build_dir, '../build/docs' + +configure :build do + activate :minify_css + activate :minify_javascript + # activate :relative_assets + # activate :asset_hash + # activate :gzip +end diff --git a/samples/rest-notes-slate/slate/font-selection.json b/samples/rest-notes-slate/slate/font-selection.json new file mode 100755 index 000000000..5e78f5d86 --- /dev/null +++ b/samples/rest-notes-slate/slate/font-selection.json @@ -0,0 +1,148 @@ +{ + "IcoMoonType": "selection", + "icons": [ + { + "icon": { + "paths": [ + "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "exclamation-circle" + ], + "defaultCode": 61546, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 100, + "order": 4, + "prevSize": 28, + "code": 58880, + "name": "exclamation-sign", + "ligatures": "" + }, + "setIdx": 0, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "info-circle" + ], + "defaultCode": 61530, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 85, + "order": 3, + "name": "info-sign", + "prevSize": 28, + "code": 58882 + }, + "setIdx": 0, + "iconIdx": 2 + }, + { + "icon": { + "paths": [ + "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "check-circle" + ], + "defaultCode": 61528, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 83, + "order": 9, + "prevSize": 28, + "code": 58886, + "name": "ok-sign" + }, + "setIdx": 0, + "iconIdx": 6 + }, + { + "icon": { + "paths": [ + "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z" + ], + "width": 951, + "attrs": [], + "isMulticolor": false, + "tags": [ + "search" + ], + "defaultCode": 61442, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 2, + "order": 1, + "prevSize": 28, + "code": 58887, + "name": "icon-search" + }, + "setIdx": 0, + "iconIdx": 7 + } + ], + "height": 1024, + "metadata": { + "name": "slate", + "license": "SIL OFL 1.1" + }, + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "icon-", + "metadata": { + "fontFamily": "slate", + "majorVersion": 1, + "minorVersion": 0, + "description": "Based on FontAwesome", + "license": "SIL OFL 1.1" + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "resetPoint": 58880, + "showSelector": false, + "selector": "class", + "classSelector": ".icon", + "showMetrics": false, + "showMetadata": true, + "showVersion": true, + "ie7": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 4473924, + "bgColor": 16777215 + }, + "historySize": 100, + "showCodes": true, + "gridSize": 16, + "showLiga": false + } +} diff --git a/samples/rest-notes-slate/slate/source/api-guide.md.erb b/samples/rest-notes-slate/slate/source/api-guide.md.erb new file mode 100644 index 000000000..11248a3ea --- /dev/null +++ b/samples/rest-notes-slate/slate/source/api-guide.md.erb @@ -0,0 +1,206 @@ +--- +title: REST Notes API Guide + +language_tabs: + - shell + - http + +search: true +--- + +# Overview + + +## HTTP verbs + +RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP verbs. + + +Verb | Usage +-------- | ----- +`GET` | Used to retrieve a resource +`POST` | Used to create a new resource +`PATCH` | Used to update an existing resource, including partial updates +`DELETE` | Used to delete an existing resource + +## HTTP status codes + +RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP status codes. + +Status code | Usage +----------------- | ----- +`200 OK` | The request completed successfully +`201 Created` | A new resource has been created successfully. The resource's URI is available from the response's `Location` header +`204 No Content` | An update to an existing resource has been applied successfully +`400 Bad Request` | The request was malformed. The response body will include an error providing further information +`404 Not Found` | The requested resource did not exist + +## Errors + +<%= ERB.new(File.read("../build/generated-snippets/error-example/http-response.md")).result(binding) %> + +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 structure: + +<%= ERB.new(File.read("../build/generated-snippets/error-example/response-fields.md")).result(binding) %> + +## Hypermedia + +RESTful Notes uses hypermedia and resources include links to other resources in their +responses. Responses are in [Hypertext Application Language (HAL)](http://stateless.co/hal_specification.html format). +Links can be found beneath the `_links` key. Users of the API should not create URIs +themselves, instead they should use the above-described links to navigate + + + +# Resources + + + +## Index + +<%= ERB.new(File.read("../build/generated-snippets/index-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/index-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/index-example/http-response.md")).result(binding) %> + +The index provides the entry point into the service. A `GET` request is used to access the index. + +### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/index-example/response-fields.md")).result(binding) %> + +### Links + +<%= ERB.new(File.read("../build/generated-snippets/index-example/links.md")).result(binding) %> + + + +## Notes + +The Notes resources is used to create and list notes + +### Listing notes + +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-response.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/curl-request.md")).result(binding) %> + +A `GET` request will list all of the service's notes. + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/response-fields.md")).result(binding) %> + +### Creating a note + +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/curl-request.md")).result(binding) %> + +A `POST` request is used to create a note + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/request-fields.md")).result(binding) %> + + + +## Tags + +The Tags resource is used to create and list tags. + +### Listing tags + +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-response.md")).result(binding) %> + +A `GET` request will list all of the service's tags. + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/response-fields.md")).result(binding) %> + + +### Creating a tag + +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> + +A `POST` request is used to create a tag + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/request-fields.md")).result(binding) %> + + + +## Note + +The Note resource is used to retrieve, update, and delete individual notes + +### Retrieve a note + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-response.md")).result(binding) %> + +A `GET` request will retrieve the details of a note + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/response-fields.md")).result(binding) %> + +#### Links + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/links.md")).result(binding) %> + +### Update a note + +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-response.md")).result(binding) %> + +A `PATCH` request is used to update a note + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/request-fields.md")).result(binding) %> + +To leave an attribute of a note unchanged, any of the above may be omitted from the +request. + + + +## Tag + +The Tag resource is used to retrieve, update, and delete individual tags + +### Retrieve a tag + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-response.md")).result(binding) %> + +A `GET` request will retrieve the details of a tag + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/response-fields.md")).result(binding) %> + +#### Links + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/links.md")).result(binding) %> + +### Update a tag + +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-response.md")).result(binding) %> + +A `PATCH` request is used to update a tag + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/request-fields.md")).result(binding) %> \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.eot b/samples/rest-notes-slate/slate/source/fonts/slate.eot new file mode 100755 index 0000000000000000000000000000000000000000..13c4839a1975d4c92d66753d75553f922743c6ef GIT binary patch literal 1876 zcmaJ?&2Jl35TEDWeRg&sP8)xqh$Kr)Yzaiu#`dnNaHtS!Xx9i53LF}#nAmmP`Xfq= z;s{Zte*qCEK;i~h4hYFPaA-x4sD#vVrBXQ{MIaD@1Ke8kn|8tnQ^fdI9N^x@? zd<=RUJW{D`U;b-v<0kYSA}zO8E|&DaCqF0BzlPme0}$TUZbAP8`m<~GR{K~75*dg= zcCEQu4DE@ZpmS=+>&5muJwb0nf0^x#V!izR7X1vpggLV7&CM3_1j&!tPMS_)mkgrN zC!rsJe5kniow8zt{RT+zltXle=pd}!=-!|+8X9a|iyqm&z#GOh#?Z4hMmoI$K1va6 zrUYgm&_U=R+`ZrJ0!LQ9E`42ef0@uHvC8U zo}w4%B?O&MCX$JGEG)w^HIqqa()pb0xK4IFpUb457c*fwDPqaQf|z%md}h1{#>ac0 zD>_@{@&c$_-fEYWRBE3yj7U8q4MTz%hA^cOxdwei%19MlXpM(P_)+e=@Rm}B>tXap zwoqO*DogaoKF+^`!zvX>{f10 zq*F6dk@0&Ok4=k2cHR|EKKaOn+_Q3KG-~~J-9n!;&D+d{W9=SQ|IqlDm9?y2uUl{( zi(0o$Q@Cby<g+B7U zE7gMM6{=S}Ps}C~lhg72%h#4X&vB-0d)je4Z)w>(?>M%-xE~jH;l@L}Lcyy(n4O9z z65lS`w&R>XbW_^$2bKN!lz(S%N3N!tcP>R={D&-^3rjyfS-aWi!AkH>sk+00G5&qW zC1%n(1Gmpd;5$IPUF_Lw@K%&WbbxEX?LgKcF9x!K$J`8LiMQ^#KsG5yOZ=|rBS1K&l2uG4tC&hwF_o-hDp_Le zTrgI}?0+pse + + +Generated by IcoMoon + + + + + + + + + + diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.ttf b/samples/rest-notes-slate/slate/source/fonts/slate.ttf new file mode 100755 index 0000000000000000000000000000000000000000..ace9a46a7e1ed6b6ab3de2f30ef3e27c572f88d3 GIT binary patch literal 1720 zcmaJ>-D_KA7=PaL@t*V?ZMro%kz(6pV-rwZyCyk@Q7?wb>UJv9`gO6wj7^qiCTUqB zO%X@;7dX69!8^V6LWJzXiw!F%45qgVdLa}=5xvm6E&iVKo-9$jJ@CHI_viaTf`}a2 zC!H2wcyVDVd0amU$>&(FZ8pn0bm7yth{U7dH)`ef4)6r{E^wmO*t_`0^~*QG?-S|8 zt!lYq{5ky*k?|Sy{uTt*p8hrX-@re<)$DYS^+1t{800m!H_O^}@g4X@@W-3w?hZXf zuY!M;^{sNV`qeJ|2)=?Gg`Mqo2XzAEd#oqjAaRXMBJF+c79{T|EPkbe7-PE;5S;Q~ zaGL1Q(r@%{&}khDI-bPX!~GUGz-0@x9Aaiik?BxrHq?#(PnWnI%nYaReOv*$ZSm>?)ctla|1hAG;T1^YPnfOv{N&_J8ekcvoG^Cm?MLpzb znO-8ASEU{s)D{<<&JKz%JjTWAM|EWWzjHMajT;ECdZ?bsKw&|^tZONrkvOuIkI%De zUYTtG_26}0jYK0>4*B1QgBuPLXU?}t^*TiboK|r`R>P0_HD+(cdi{Ze{FKYDLBs0R~?v6B%Rx~Edo2aasT@IJ-vtfG(iE^ z$Awv_E5l{^C4s84a|;3+->t#z!u^V-9Nj!@+Ph(RslFP9tMyA^DCS*vdNzG<@yc2l z`u?ov&H8>AuC0gXeBbj{4$|U#n6XQ^x*FE+&d;P>_lp(J^Zj%8%oMl&cI_ZN6TKO{ zkvFp2-&{yO{TDd~50<`txN&oc<4*8TskuV~pXj~g5i{t$k=GYVU^@bQTx>a5uvcK? zADE$i`dge4A3((KH9;?{zv*7K*f>Jt^humckR5yQeHf=xv0R7Ti)jP&N=%#Ng5wPM z_VCv|5z{smX^sCCV+<0Gsc02b(JH2*RZK;zn2MJ0U5I^E%r-TsOdYDyD_EDQO?BF) z7OFc*Cuk9TtZz5Uo$8RKb(q)a%}C8|gD__z_YMNoV9|<#sst6tUZ*|mXK52w&tq|_ J6Wq-M;U6NW`v(93 literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.woff b/samples/rest-notes-slate/slate/source/fonts/slate.woff new file mode 100755 index 0000000000000000000000000000000000000000..1e72e0ee0018119d7c814c2097cc60c0bcd05d84 GIT binary patch literal 1796 zcmaJ>&2Jl35TCanZ=GF;-Nqj%Qj#SmwgjS~vAs(b4i$%lc7v*fuR|jhlemtH?Nqf< z5&;$c3y8RZ#0{p83^QtGf$~^ZU#vdPL3b z4fcBr_DN>rKz!G#?&4f~pM4ZRM6a}~ts1aTadwIM%N_dh>UO7#K7YRFXF@YA68l_? z@xFm7>0K?wZ&VUvut!OxMlIIQ5*<0_&Hha~Yl49Y@PK@!7+CqFG*;eClSR)#j$=Xw zNnrjF9T`VX|4zRT99<||DqHk_nzSa(NzO5voBad{L?lOWoE4r?ZbRP(V_X@TZL>{} z(3A8mk}l-3xojrwNJr*pi-lsLQVxSKC{0w##ljO}){#>poy#tYg)pcTCk9|L<3To?f*omEO$b9ODUa}gVj!a zAvUB1l6OCpmTg;7PgnO)phbF-Xik@UVo+OLa3((}zVs*;Zywi?{r}GOL=0{q1ou!q ztD%;HAbGE?Z5HC#RzAMSTXWiN9ioS*i+Usm@#fI}eK@$`F!8DQHtAj`iEnm!UKH}P zNl{d*%%o>TwzLq6ppv_9BR_a$H<|Q)z2RXkyY6k4BJlQ)o4+xU@=Bif%MA~%sib$? zbw%hV*Y96nzi0MvpHdWZeO#D>x^i4rP!XsqKRYk5@ZB2RF5E9QWp(qg81F^VmBvaG ztu(Ggk(kS7r)DyTm#?ozQ4q}d{!9==(dt@sJ_vk&`7k5ChZ~1PD=Sgs?%Z@HoBe#* z_k-a4JvVKwyWWZ=q2@1#9uk~9EfrHBtA=!8%MC` zIGu@c6SyDi7WCym@ z2-w*rE$c_UfviKEL=Vy>={0bo+(i5#P1q3$6VB``hVMrCzU@B6vbqKcPy}m-BtQn- zkylU>byNe6DE>18g5d9bm%bZqa|qF=7&3(|U!AY)%Yg-vOsqKIq$w0D$r*3}ks4=+ z>U6?w2xMd=B$;zQ9v)F1K7KwfQ9|)<9nl&i?6-rchs{I8LC1eK^@#(|^QwB-{VeQz zoHLn@cwG?Yo(X!F(Q8iBOqSNs${h}C*?qJYLuZAnVXqx{ww|}Ux0`ca^V#hz?c7{r z7ldYbmk!M1m3G+IYC6(!VcCzqp&pJkZ#1@kw2W<^EJMqOM;^&dq6ELC*|pK!(cC0< zbZtF%BzLsuTw5O*+eg;dX0a+hcfI=gj8a~qWYJoMamHAo)!Qq|6&~9S8`{d0T?L&% z@$R=P6H0rTp6V+-Dlb>EB67JEMQ>kk%`y}{hvBC(RNEw)*%|U|4>SL+B=3rE@y*rw zR_bLZr&~XN?C;+yx2IilcDnrb?M)}An;DsTe@^rq$9L-UWNqY_oTyWyYisN3CMIh; z*C)b12exYgK^2c4S}J#2YJ+00rUlj%+LqQ7E%lldEc`K|1Bky)iLkwF{Wltvj!{A+PBEzhE3BX) zFBgqmj@&z?N1*=OK?z54T7is8FiA(N66oe`X+cKl>`n&&000000HC;3 zk^ug-#56-JW5JAtMV6Rgj#+|9A(7;@^`+^dWy@Alt7D86w|;R>QMs>d+47HJVfKhH aD;vc&%m&dlj4($-ipx}q&#}A+00017%3vV? literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1f13da8193dacca747df56cc1f7bb0bc111a7e GIT binary patch literal 3507 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 literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/images/navbar.png b/samples/rest-notes-slate/slate/source/images/navbar.png new file mode 100644 index 0000000000000000000000000000000000000000..df38e90d87e1a215371b4977e18cde90f8832537 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk@BpAX3RW*PVQ%R6tFatx`= 1) { + var language = parseURL(location.search).language + if (language) { + return language; + } else if (jQuery.inArray(location.search.substr(1), languages) != -1) { + return location.search.substr(1); + } + } + + return false; + } + + // returns a new query string with the new language in it + function generateNewQueryString(language) { + var url = parseURL(location.search); + if (url.language) { + url.language = language; + return stringifyURL(url); + } + return language; + } + + // if a button is clicked, add the state to the history + function pushURL(language) { + if (!history) { return; } + var hash = window.location.hash; + if (hash) { + hash = hash.replace(/^#+/, ''); + } + history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash); + + // save language as next default + localStorage.setItem("language", language); + } + + function setupLanguages(l) { + var defaultLanguage = localStorage.getItem("language"); + + languages = l; + + var presetLanguage = getLanguageFromQueryString(); + if (presetLanguage) { + // the language is in the URL, so use that language! + activateLanguage(presetLanguage); + + localStorage.setItem("language", presetLanguage); + } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { + // the language was the last selected one saved in localstorage, so use that language! + activateLanguage(defaultLanguage); + } else { + // no language selected, so use the default + activateLanguage(languages[0]); + } + } + + // if we click on a language tab, activate that language + $(function() { + $(".lang-selector a").on("click", function() { + var language = $(this).data("language-name"); + pushURL(language); + activateLanguage(language); + return false; + }); + window.onpopstate = function() { + activateLanguage(getLanguageFromQueryString()); + }; + }); +})(window); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js new file mode 100644 index 000000000..91f38a04e --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js @@ -0,0 +1,74 @@ +//= require ../lib/_lunr +//= require ../lib/_jquery.highlight +(function () { + 'use strict'; + + var content, searchResults; + var highlightOpts = { element: 'span', className: 'search-highlight' }; + + var index = new lunr.Index(); + + index.ref('id'); + index.field('title', { boost: 10 }); + index.field('body'); + index.pipeline.add(lunr.trimmer, lunr.stopWordFilter); + + $(populate); + $(bind); + + function populate() { + $('h1, h2').each(function() { + var title = $(this); + var body = title.nextUntil('h1, h2'); + index.add({ + id: title.prop('id'), + title: title.text(), + body: body.text() + }); + }); + } + + function bind() { + content = $('.content'); + searchResults = $('.search-results'); + + $('#input-search').on('keyup', search); + } + + function search(event) { + unhighlight(); + searchResults.addClass('visible'); + + // ESC clears the field + if (event.keyCode === 27) this.value = ''; + + if (this.value) { + var results = index.search(this.value).filter(function(r) { + return r.score > 0.0001; + }); + + if (results.length) { + searchResults.empty(); + $.each(results, function (index, result) { + var elem = document.getElementById(result.ref); + searchResults.append("

    • " + $(elem).text() + "
    • "); + }); + highlight.call(this); + } else { + searchResults.html('
    • '); + $('.search-results li').text('No Results Found for "' + this.value + '"'); + } + } else { + unhighlight(); + searchResults.removeClass('visible'); + } + } + + function highlight() { + if (this.value) content.highlight(this.value, highlightOpts); + } + + function unhighlight() { + content.unhighlight(highlightOpts); + } +})(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js new file mode 100644 index 000000000..bc2aa3e1f --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js @@ -0,0 +1,55 @@ +//= require ../lib/_jquery_ui +//= require ../lib/_jquery.tocify +//= require ../lib/_imagesloaded.min +(function (global) { + 'use strict'; + + var closeToc = function() { + $(".tocify-wrapper").removeClass('open'); + $("#nav-button").removeClass('open'); + }; + + var makeToc = function() { + global.toc = $("#toc").tocify({ + selectors: 'h1, h2', + extendPage: false, + theme: 'none', + smoothScroll: false, + showEffectSpeed: 0, + hideEffectSpeed: 180, + ignoreSelector: '.toc-ignore', + highlightOffset: 60, + scrollTo: -1, + scrollHistory: true, + hashGenerator: function (text, element) { + return element.prop('id'); + } + }).data('toc-tocify'); + + $("#nav-button").click(function() { + $(".tocify-wrapper").toggleClass('open'); + $("#nav-button").toggleClass('open'); + return false; + }); + + $(".page-wrapper").click(closeToc); + $(".tocify-item").click(closeToc); + }; + + // Hack to make already open sections to start opened, + // instead of displaying an ugly animation + function animate() { + setTimeout(function() { + toc.setOption('showEffectSpeed', 180); + }, 50); + } + + $(function() { + makeToc(); + animate(); + $('.content').imagesLoaded( function() { + global.toc.calculateHeights(); + }); + }); +})(window); + diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js new file mode 100644 index 000000000..6798f3c03 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js @@ -0,0 +1,169 @@ +/** + * energize.js v0.1.0 + * + * Speeds up click events on mobile devices. + * https://github.com/davidcalhoun/energize.js + */ + +(function() { // Sandbox + /** + * Don't add to non-touch devices, which don't need to be sped up + */ + if(!('ontouchstart' in window)) return; + + var lastClick = {}, + isThresholdReached, touchstart, touchmove, touchend, + click, closest; + + /** + * isThresholdReached + * + * Compare touchstart with touchend xy coordinates, + * and only fire simulated click event if the coordinates + * are nearby. (don't want clicking to be confused with a swipe) + */ + isThresholdReached = function(startXY, xy) { + return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; + }; + + /** + * touchstart + * + * Save xy coordinates when the user starts touching the screen + */ + touchstart = function(e) { + this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; + this.threshold = false; + }; + + /** + * touchmove + * + * Check if the user is scrolling past the threshold. + * Have to check here because touchend will not always fire + * on some tested devices (Kindle Fire?) + */ + touchmove = function(e) { + // NOOP if the threshold has already been reached + if(this.threshold) return false; + + this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); + }; + + /** + * touchend + * + * If the user didn't scroll past the threshold between + * touchstart and touchend, fire a simulated click. + * + * (This will fire before a native click) + */ + touchend = function(e) { + // Don't fire a click if the user scrolled past the threshold + if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { + return; + } + + /** + * Create and fire a click event on the target element + * https://developer.mozilla.org/en/DOM/event.initMouseEvent + */ + var touch = e.changedTouches[0], + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + evt.simulated = true; // distinguish from a normal (nonsimulated) click + e.target.dispatchEvent(evt); + }; + + /** + * click + * + * Because we've already fired a click event in touchend, + * we need to listed for all native click events here + * and suppress them as necessary. + */ + click = function(e) { + /** + * Prevent ghost clicks by only allowing clicks we created + * in the click event we fired (look for e.simulated) + */ + var time = Date.now(), + timeDiff = time - lastClick.time, + x = e.clientX, + y = e.clientY, + xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], + target = closest(e.target, 'A') || e.target, // needed for standalone apps + nodeName = target.nodeName, + isLink = nodeName === 'A', + standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); + + lastClick.time = time; + lastClick.x = x; + lastClick.y = y; + + /** + * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), + * so we have to add more logic to determine the time of the last click. Not perfect... + * + * Older, simpler check: if((!e.simulated) || standAlone) + */ + if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { + e.preventDefault(); + e.stopPropagation(); + if(!standAlone) return false; + } + + /** + * Special logic for standalone web apps + * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window + */ + if(standAlone) { + window.location = target.getAttribute("href"); + } + + /** + * Add an energize-focus class to the targeted link (mimics :focus behavior) + * TODO: test and/or remove? Does this work? + */ + if(!target || !target.classList) return; + target.classList.add("energize-focus"); + window.setTimeout(function(){ + target.classList.remove("energize-focus"); + }, 150); + }; + + /** + * closest + * @param {HTMLElement} node current node to start searching from. + * @param {string} tagName the (uppercase) name of the tag you're looking for. + * + * Find the closest ancestor tag of a given node. + * + * Starts at node and goes up the DOM tree looking for a + * matching nodeName, continuing until hitting document.body + */ + closest = function(node, tagName){ + var curNode = node; + + while(curNode !== document.body) { // go up the dom until we find the tag we're after + if(!curNode || curNode.nodeName === tagName) { return curNode; } // found + curNode = curNode.parentNode; // not found, so keep going up + } + + return null; // not found + }; + + /** + * Add all delegated event listeners + * + * All the events we care about bubble up to document, + * so we can take advantage of event delegation. + * + * Note: no need to wait for DOMContentLoaded here + */ + document.addEventListener('touchstart', touchstart, false); + document.addEventListener('touchmove', touchmove, false); + document.addEventListener('touchend', touchend, false); + document.addEventListener('click', click, true); // TODO: why does this use capture? + +})(); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js new file mode 100644 index 000000000..d66f65893 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js @@ -0,0 +1,7 @@ +/*! + * imagesLoaded PACKAGED v3.1.8 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +(function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s}); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js new file mode 100644 index 000000000..9dcf3c7af --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js @@ -0,0 +1,108 @@ +/* + * jQuery Highlight plugin + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * Code a little bit refactored and cleaned (in my humble opinion). + * Most important changes: + * - has an option to highlight only entire words (wordsOnly - false by default), + * - has an option to be case sensitive (caseSensitive - false by default) + * - highlight element tag and class names can be specified in options + * + * Usage: + * // wrap every occurrance of text 'lorem' in content + * // with (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word, i){ + return word != ''; + }); + words = jQuery.map(words, function(word, i) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length == 0) { return this; }; + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; + diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js new file mode 100644 index 000000000..91cf63791 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js @@ -0,0 +1,1042 @@ +/* jquery Tocify - v1.8.0 - 2013-09-16 +* http://www.gregfranko.com/jquery.tocify.js/ +* Copyright (c) 2013 Greg Franko; Licensed MIT +* Modified lightly by Robert Lord to fix a bug I found, +* and also so it adds ids to headers +* also because I want height caching, since the +* height lookup for h1s and h2s was causing serious +* lag spikes below 30 fps */ + +// Immediately-Invoked Function Expression (IIFE) [Ben Alman Blog Post](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) that calls another IIFE that contains all of the plugin logic. I used this pattern so that anyone viewing this code would not have to scroll to the bottom of the page to view the local parameters that were passed to the main IIFE. +(function(tocify) { + + // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) + "use strict"; + + // Calls the second IIFE and locally passes in the global jQuery, window, and document objects + tocify(window.jQuery, window, document); + +} + +// Locally passes in `jQuery`, the `window` object, the `document` object, and an `undefined` variable. The `jQuery`, `window` and `document` objects are passed in locally, to improve performance, since javascript first searches for a variable match within the local variables set before searching the global variables set. All of the global variables are also passed in locally to be minifier friendly. `undefined` can be passed in locally, because it is not a reserved word in JavaScript. +(function($, window, document, undefined) { + + // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) + "use strict"; + + var tocClassName = "tocify", + tocClass = "." + tocClassName, + tocFocusClassName = "tocify-focus", + tocHoverClassName = "tocify-hover", + hideTocClassName = "tocify-hide", + hideTocClass = "." + hideTocClassName, + headerClassName = "tocify-header", + headerClass = "." + headerClassName, + subheaderClassName = "tocify-subheader", + subheaderClass = "." + subheaderClassName, + itemClassName = "tocify-item", + itemClass = "." + itemClassName, + extendPageClassName = "tocify-extend-page", + extendPageClass = "." + extendPageClassName; + + // Calling the jQueryUI Widget Factory Method + $.widget("toc.tocify", { + + //Plugin version + version: "1.8.0", + + // These options will be used as defaults + options: { + + // **context**: Accepts String: Any jQuery selector + // The container element that holds all of the elements used to generate the table of contents + context: "body", + + // **ignoreSelector**: Accepts String: Any jQuery selector + // A selector to any element that would be matched by selectors that you wish to be ignored + ignoreSelector: null, + + // **selectors**: Accepts an Array of Strings: Any jQuery selectors + // The element's used to generate the table of contents. The order is very important since it will determine the table of content's nesting structure + selectors: "h1, h2, h3", + + // **showAndHide**: Accepts a boolean: true or false + // Used to determine if elements should be shown and hidden + showAndHide: true, + + // **showEffect**: Accepts String: "none", "fadeIn", "show", or "slideDown" + // Used to display any of the table of contents nested items + showEffect: "slideDown", + + // **showEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the show animation + showEffectSpeed: "medium", + + // **hideEffect**: Accepts String: "none", "fadeOut", "hide", or "slideUp" + // Used to hide any of the table of contents nested items + hideEffect: "slideUp", + + // **hideEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the hide animation + hideEffectSpeed: "medium", + + // **smoothScroll**: Accepts a boolean: true or false + // Determines if a jQuery animation should be used to scroll to specific table of contents items on the page + smoothScroll: true, + + // **smoothScrollSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the smoothScroll animation + smoothScrollSpeed: "medium", + + // **scrollTo**: Accepts Number (pixels) + // The amount of space between the top of page and the selected table of contents item after the page has been scrolled + scrollTo: 0, + + // **showAndHideOnScroll**: Accepts a boolean: true or false + // Determines if table of contents nested items should be shown and hidden while scrolling + showAndHideOnScroll: true, + + // **highlightOnScroll**: Accepts a boolean: true or false + // Determines if table of contents nested items should be highlighted (set to a different color) while scrolling + highlightOnScroll: true, + + // **highlightOffset**: Accepts a number + // The offset distance in pixels to trigger the next active table of contents item + highlightOffset: 40, + + // **theme**: Accepts a string: "bootstrap", "jqueryui", or "none" + // Determines if Twitter Bootstrap, jQueryUI, or Tocify classes should be added to the table of contents + theme: "bootstrap", + + // **extendPage**: Accepts a boolean: true or false + // If a user scrolls to the bottom of the page and the page is not tall enough to scroll to the last table of contents item, then the page height is increased + extendPage: true, + + // **extendPageOffset**: Accepts a number: pixels + // How close to the bottom of the page a user must scroll before the page is extended + extendPageOffset: 100, + + // **history**: Accepts a boolean: true or false + // Adds a hash to the page url to maintain history + history: true, + + // **scrollHistory**: Accepts a boolean: true or false + // Adds a hash to the page url, to maintain history, when scrolling to a TOC item + scrollHistory: false, + + // **hashGenerator**: How the hash value (the anchor segment of the URL, following the + // # character) will be generated. + // + // "compact" (default) - #CompressesEverythingTogether + // "pretty" - #looks-like-a-nice-url-and-is-easily-readable + // function(text, element){} - Your own hash generation function that accepts the text as an + // argument, and returns the hash value. + hashGenerator: "compact", + + // **highlightDefault**: Accepts a boolean: true or false + // Set's the first TOC item as active if no other TOC item is active. + highlightDefault: true + + }, + + // _Create + // ------- + // Constructs the plugin. Only called once. + _create: function() { + + var self = this; + + self.tocifyWrapper = $('.tocify-wrapper'); + self.extendPageScroll = true; + + // Internal array that keeps track of all TOC items (Helps to recognize if there are duplicate TOC item strings) + self.items = []; + + // Generates the HTML for the dynamic table of contents + self._generateToc(); + + // Caches heights and anchors + self.cachedHeights = [], + self.cachedAnchors = []; + + // Adds CSS classes to the newly generated table of contents HTML + self._addCSSClasses(); + + self.webkit = (function() { + + for(var prop in window) { + + if(prop) { + + if(prop.toLowerCase().indexOf("webkit") !== -1) { + + return true; + + } + + } + + } + + return false; + + }()); + + // Adds jQuery event handlers to the newly generated table of contents + self._setEventHandlers(); + + // Binding to the Window load event to make sure the correct scrollTop is calculated + $(window).load(function() { + + // Sets the active TOC item + self._setActiveElement(true); + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + setTimeout(function() { + + self.extendPageScroll = false; + + },0); + + }); + + }); + + }, + + // _generateToc + // ------------ + // Generates the HTML for the dynamic table of contents + _generateToc: function() { + + // _Local variables_ + + // Stores the plugin context in the self variable + var self = this, + + // All of the HTML tags found within the context provided (i.e. body) that match the top level jQuery selector above + firstElem, + + // Instantiated variable that will store the top level newly created unordered list DOM element + ul, + ignoreSelector = self.options.ignoreSelector; + + // If the selectors option has a comma within the string + if(this.options.selectors.indexOf(",") !== -1) { + + // Grabs the first selector from the string + firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"").substr(0, this.options.selectors.indexOf(","))); + + } + + // If the selectors option does not have a comman within the string + else { + + // Grabs the first selector from the string and makes sure there are no spaces + firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"")); + + } + + if(!firstElem.length) { + + self.element.addClass(hideTocClassName); + + return; + + } + + self.element.addClass(tocClassName); + + // Loops through each top level selector + firstElem.each(function(index) { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + // Creates an unordered list HTML element and adds a dynamic ID and standard class name + ul = $("
        ", { + "id": headerClassName + index, + "class": headerClassName + }). + + // Appends a top level list item HTML element to the previously created HTML header + append(self._nestElements($(this), index)); + + // Add the created unordered list element to the HTML element calling the plugin + self.element.append(ul); + + // Finds all of the HTML tags between the header and subheader elements + $(this).nextUntil(this.nodeName.toLowerCase()).each(function() { + + // If there are no nested subheader elemements + if($(this).find(self.options.selectors).length === 0) { + + // Loops through all of the subheader elements + $(this).filter(self.options.selectors).each(function() { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + self._appendSubheaders.call(this, self, ul); + + }); + + } + + // If there are nested subheader elements + else { + + // Loops through all of the subheader elements + $(this).find(self.options.selectors).each(function() { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + self._appendSubheaders.call(this, self, ul); + + }); + + } + + }); + + }); + + }, + + _setActiveElement: function(pageload) { + + var self = this, + + hash = window.location.hash.substring(1), + + elem = self.element.find("li[data-unique='" + hash + "']"); + + if(hash.length) { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the current list item that was clicked + elem.addClass(self.focusClass); + + // If the showAndHide option is true + if(self.options.showAndHide) { + + // Triggers the click event on the currently focused TOC item + elem.click(); + + } + + } + + else { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + if(!hash.length && pageload && self.options.highlightDefault) { + + // Highlights the first TOC item if no other items are highlighted + self.element.find(itemClass).first().addClass(self.focusClass); + + } + + } + + return self; + + }, + + // _nestElements + // ------------- + // Helps create the table of contents list by appending nested list items + _nestElements: function(self, index) { + + var arr, item, hashValue; + + arr = $.grep(this.items, function (item) { + + return item === self.text(); + + }); + + // If there is already a duplicate TOC item + if(arr.length) { + + // Adds the current TOC item text and index (for slight randomization) to the internal array + this.items.push(self.text() + index); + + } + + // If there not a duplicate TOC item + else { + + // Adds the current TOC item text to the internal array + this.items.push(self.text()); + + } + + hashValue = this._generateHashValue(arr, self, index); + + // ADDED BY ROBERT + // actually add the hash value to the element's id + // self.attr("id", "link-" + hashValue); + + // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin + item = $("
      • ", { + + // Sets a common class name to the list item + "class": itemClassName, + + "data-unique": hashValue + + }).append($("", { + + "text": self.text() + + })); + + // Adds an HTML anchor tag before the currently traversed HTML element + self.before($("
        ", { + + // Sets a name attribute on the anchor tag to the text of the currently traversed HTML element (also making sure that all whitespace is replaced with an underscore) + "name": hashValue, + + "data-unique": hashValue + + })); + + return item; + + }, + + // _generateHashValue + // ------------------ + // Generates the hash value that will be used to refer to each item. + _generateHashValue: function(arr, self, index) { + + var hashValue = "", + hashGeneratorOption = this.options.hashGenerator; + + if (hashGeneratorOption === "pretty") { + // remove weird characters + + + // prettify the text + hashValue = self.text().toLowerCase().replace(/\s/g, "-"); + + // ADDED BY ROBERT + // remove weird characters + hashValue = hashValue.replace(/[^\x00-\x7F]/g, ""); + + // fix double hyphens + while (hashValue.indexOf("--") > -1) { + hashValue = hashValue.replace(/--/g, "-"); + } + + // fix colon-space instances + while (hashValue.indexOf(":-") > -1) { + hashValue = hashValue.replace(/:-/g, "-"); + } + + } else if (typeof hashGeneratorOption === "function") { + + // call the function + hashValue = hashGeneratorOption(self.text(), self); + + } else { + + // compact - the default + hashValue = self.text().replace(/\s/g, ""); + + } + + // add the index if we need to + if (arr.length) { hashValue += ""+index; } + + // return the value + return hashValue; + + }, + + // _appendElements + // --------------- + // Helps create the table of contents list by appending subheader elements + + _appendSubheaders: function(self, ul) { + + // The current element index + var index = $(this).index(self.options.selectors), + + // Finds the previous header DOM element + previousHeader = $(self.options.selectors).eq(index - 1), + + currentTagName = +$(this).prop("tagName").charAt(1), + + previousTagName = +previousHeader.prop("tagName").charAt(1), + + lastSubheader; + + // If the current header DOM element is smaller than the previous header DOM element or the first subheader + if(currentTagName < previousTagName) { + + // Selects the last unordered list HTML found within the HTML element calling the plugin + self.element.find(subheaderClass + "[data-tag=" + currentTagName + "]").last().append(self._nestElements($(this), index)); + + } + + // If the current header DOM element is the same type of header(eg. h4) as the previous header DOM element + else if(currentTagName === previousTagName) { + + ul.find(itemClass).last().after(self._nestElements($(this), index)); + + } + + else { + + // Selects the last unordered list HTML found within the HTML element calling the plugin + ul.find(itemClass).last(). + + // Appends an unorderedList HTML element to the dynamic `unorderedList` variable and sets a common class name + after($("
          ", { + + "class": subheaderClassName, + + "data-tag": currentTagName + + })).next(subheaderClass). + + // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin + append(self._nestElements($(this), index)); + } + + }, + + // _setEventHandlers + // ---------------- + // Adds jQuery event handlers to the newly generated table of contents + _setEventHandlers: function() { + + // _Local variables_ + + // Stores the plugin context in the self variable + var self = this, + + // Instantiates a new variable that will be used to hold a specific element's context + $self, + + // Instantiates a new variable that will be used to determine the smoothScroll animation time duration + duration; + + // Event delegation that looks for any clicks on list item elements inside of the HTML element calling the plugin + this.element.on("click.tocify", "li", function(event) { + + if(self.options.history) { + + window.location.hash = $(this).attr("data-unique"); + + } + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the current list item that was clicked + $(this).addClass(self.focusClass); + + // If the showAndHide option is true + if(self.options.showAndHide) { + + var elem = $('li[data-unique="' + $(this).attr("data-unique") + '"]'); + + self._triggerShow(elem); + + } + + self._scrollTo($(this)); + + }); + + // Mouseenter and Mouseleave event handlers for the list item's within the HTML element calling the plugin + this.element.find("li").on({ + + // Mouseenter event handler + "mouseenter.tocify": function() { + + // Adds a hover CSS class to the current list item + $(this).addClass(self.hoverClass); + + // Makes sure the cursor is set to the pointer icon + $(this).css("cursor", "pointer"); + + }, + + // Mouseleave event handler + "mouseleave.tocify": function() { + + if(self.options.theme !== "bootstrap") { + + // Removes the hover CSS class from the current list item + $(this).removeClass(self.hoverClass); + + } + + } + }); + + // Reset height cache on scroll + + $(window).on('resize', function() { + self.calculateHeights(); + }); + + // Window scroll event handler + $(window).on("scroll.tocify", function() { + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + // Local variables + + // Stores how far the user has scrolled + var winScrollTop = $(window).scrollTop(), + + // Stores the height of the window + winHeight = $(window).height(), + + // Stores the height of the document + docHeight = $(document).height(), + + scrollHeight = $("body")[0].scrollHeight, + + // Instantiates a variable that will be used to hold a selected HTML element + elem, + + lastElem, + + lastElemOffset, + + currentElem; + + if(self.options.extendPage) { + + // If the user has scrolled to the bottom of the page and the last toc item is not focused + if((self.webkit && winScrollTop >= scrollHeight - winHeight - self.options.extendPageOffset) || (!self.webkit && winHeight + winScrollTop > docHeight - self.options.extendPageOffset)) { + + if(!$(extendPageClass).length) { + + lastElem = $('div[data-unique="' + $(itemClass).last().attr("data-unique") + '"]'); + + if(!lastElem.length) return; + + // Gets the top offset of the page header that is linked to the last toc item + lastElemOffset = lastElem.offset().top; + + // Appends a div to the bottom of the page and sets the height to the difference of the window scrollTop and the last element's position top offset + $(self.options.context).append($("
          ", { + + "class": extendPageClassName, + + "height": Math.abs(lastElemOffset - winScrollTop) + "px", + + "data-unique": extendPageClassName + + })); + + if(self.extendPageScroll) { + + currentElem = self.element.find('li.active'); + + self._scrollTo($("div[data-unique=" + currentElem.attr("data-unique") + "]")); + + } + + } + + } + + } + + // The zero timeout ensures the following code is run after the scroll events + setTimeout(function() { + + // _Local variables_ + + // Stores the distance to the closest anchor + var // Stores the index of the closest anchor + closestAnchorIdx = null, + anchorText; + + // if never calculated before, calculate and cache the heights + if (self.cachedHeights.length == 0) { + self.calculateHeights(); + } + + var scrollTop = $(window).scrollTop(); + + // Determines the index of the closest anchor + self.cachedAnchors.each(function(idx) { + if (self.cachedHeights[idx] - scrollTop < 0) { + closestAnchorIdx = idx; + } else { + return false; + } + }); + + anchorText = $(self.cachedAnchors[closestAnchorIdx]).attr("data-unique"); + + // Stores the list item HTML element that corresponds to the currently traversed anchor tag + elem = $('li[data-unique="' + anchorText + '"]'); + + // If the `highlightOnScroll` option is true and a next element is found + if(self.options.highlightOnScroll && elem.length && !elem.hasClass(self.focusClass)) { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the corresponding list item + elem.addClass(self.focusClass); + + // Scroll to highlighted element's header + var tocifyWrapper = self.tocifyWrapper; + var scrollToElem = $(elem).closest('.tocify-header'); + + var elementOffset = scrollToElem.offset().top, + wrapperOffset = tocifyWrapper.offset().top; + var offset = elementOffset - wrapperOffset; + + if (offset >= $(window).height()) { + var scrollPosition = offset + tocifyWrapper.scrollTop(); + tocifyWrapper.scrollTop(scrollPosition); + } else if (offset < 0) { + tocifyWrapper.scrollTop(0); + } + } + + if(self.options.scrollHistory) { + + // IF STATEMENT ADDED BY ROBERT + + if(window.location.hash !== "#" + anchorText && anchorText !== undefined) { + + if(history.replaceState) { + history.replaceState({}, "", "#" + anchorText); + // provide a fallback + } else { + scrollV = document.body.scrollTop; + scrollH = document.body.scrollLeft; + location.hash = "#" + anchorText; + document.body.scrollTop = scrollV; + document.body.scrollLeft = scrollH; + } + + } + + } + + // If the `showAndHideOnScroll` option is true + if(self.options.showAndHideOnScroll && self.options.showAndHide) { + + self._triggerShow(elem, true); + + } + + }, 0); + + }); + + }); + + }, + + // calculateHeights + // ---- + // ADDED BY ROBERT + calculateHeights: function() { + var self = this; + self.cachedHeights = []; + self.cachedAnchors = []; + var anchors = $(self.options.context).find("div[data-unique]"); + anchors.each(function(idx) { + var distance = (($(this).next().length ? $(this).next() : $(this)).offset().top - self.options.highlightOffset); + self.cachedHeights[idx] = distance; + }); + self.cachedAnchors = anchors; + }, + + // Show + // ---- + // Opens the current sub-header + show: function(elem, scroll) { + + // Stores the plugin context in the `self` variable + var self = this, + element = elem; + + // If the sub-header is not already visible + if (!elem.is(":visible")) { + + // If the current element does not have any nested subheaders, is not a header, and its parent is not visible + if(!elem.find(subheaderClass).length && !elem.parent().is(headerClass) && !elem.parent().is(":visible")) { + + // Sets the current element to all of the subheaders within the current header + elem = elem.parents(subheaderClass).add(elem); + + } + + // If the current element does not have any nested subheaders and is not a header + else if(!elem.children(subheaderClass).length && !elem.parent().is(headerClass)) { + + // Sets the current element to the closest subheader + elem = elem.closest(subheaderClass); + + } + + //Determines what jQuery effect to use + switch (self.options.showEffect) { + + //Uses `no effect` + case "none": + + elem.show(); + + break; + + //Uses the jQuery `show` special effect + case "show": + + elem.show(self.options.showEffectSpeed); + + break; + + //Uses the jQuery `slideDown` special effect + case "slideDown": + + elem.slideDown(self.options.showEffectSpeed); + + break; + + //Uses the jQuery `fadeIn` special effect + case "fadeIn": + + elem.fadeIn(self.options.showEffectSpeed); + + break; + + //If none of the above options were passed, then a `jQueryUI show effect` is expected + default: + + elem.show(); + + break; + + } + + } + + // If the current subheader parent element is a header + if(elem.parent().is(headerClass)) { + + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem)); + + } + + // If the current subheader parent element is not a header + else { + + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem.closest(headerClass).find(subheaderClass).not(elem.siblings()))); + + } + + // Maintains chainablity + return self; + + }, + + // Hide + // ---- + // Closes the current sub-header + hide: function(elem) { + + // Stores the plugin context in the `self` variable + var self = this; + + //Determines what jQuery effect to use + switch (self.options.hideEffect) { + + // Uses `no effect` + case "none": + + elem.hide(); + + break; + + // Uses the jQuery `hide` special effect + case "hide": + + elem.hide(self.options.hideEffectSpeed); + + break; + + // Uses the jQuery `slideUp` special effect + case "slideUp": + + elem.slideUp(self.options.hideEffectSpeed); + + break; + + // Uses the jQuery `fadeOut` special effect + case "fadeOut": + + elem.fadeOut(self.options.hideEffectSpeed); + + break; + + // If none of the above options were passed, then a `jqueryUI hide effect` is expected + default: + + elem.hide(); + + break; + + } + + // Maintains chainablity + return self; + }, + + // _triggerShow + // ------------ + // Determines what elements get shown on scroll and click + _triggerShow: function(elem, scroll) { + + var self = this; + + // If the current element's parent is a header element or the next element is a nested subheader element + if(elem.parent().is(headerClass) || elem.next().is(subheaderClass)) { + + // Shows the next sub-header element + self.show(elem.next(subheaderClass), scroll); + + } + + // If the current element's parent is a subheader element + else if(elem.parent().is(subheaderClass)) { + + // Shows the parent sub-header element + self.show(elem.parent(), scroll); + + } + + // Maintains chainability + return self; + + }, + + // _addCSSClasses + // -------------- + // Adds CSS classes to the newly generated table of contents HTML + _addCSSClasses: function() { + + // If the user wants a jqueryUI theme + if(this.options.theme === "jqueryui") { + + this.focusClass = "ui-state-default"; + + this.hoverClass = "ui-state-hover"; + + //Adds the default styling to the dropdown list + this.element.addClass("ui-widget").find(".toc-title").addClass("ui-widget-header").end().find("li").addClass("ui-widget-content"); + + } + + // If the user wants a twitterBootstrap theme + else if(this.options.theme === "bootstrap") { + + this.element.find(headerClass + "," + subheaderClass).addClass("nav nav-list"); + + this.focusClass = "active"; + + } + + // If a user does not want a prebuilt theme + else { + + // Adds more neutral classes (instead of jqueryui) + + this.focusClass = tocFocusClassName; + + this.hoverClass = tocHoverClassName; + + } + + //Maintains chainability + return this; + + }, + + // setOption + // --------- + // Sets a single Tocify option after the plugin is invoked + setOption: function() { + + // Calls the jQueryUI Widget Factory setOption method + $.Widget.prototype._setOption.apply(this, arguments); + + }, + + // setOptions + // ---------- + // Sets a single or multiple Tocify options after the plugin is invoked + setOptions: function() { + + // Calls the jQueryUI Widget Factory setOptions method + $.Widget.prototype._setOptions.apply(this, arguments); + + }, + + // _scrollTo + // --------- + // Scrolls to a specific element + _scrollTo: function(elem) { + + var self = this, + duration = self.options.smoothScroll || 0, + scrollTo = self.options.scrollTo; + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + // Animates the html and body element scrolltops + $("html, body").animate({ + + // Sets the jQuery `scrollTop` to the top offset of the HTML div tag that matches the current list item's `data-unique` tag + "scrollTop": $('div[data-unique="' + elem.attr("data-unique") + '"]').next().offset().top - ($.isFunction(scrollTo) ? scrollTo.call() : scrollTo) + "px" + + }, { + + // Sets the smoothScroll animation time duration to the smoothScrollSpeed option + "duration": duration + + }); + + }); + + // Maintains chainability + return self; + + } + + }); + +})); //end of plugin diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js new file mode 100644 index 000000000..637e9c142 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js @@ -0,0 +1,566 @@ +/*! jQuery UI - v1.11.3 - 2015-02-12 + * http://jqueryui.com + * Includes: widget.js + * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + /*! + * jQuery UI Widget 1.11.3 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + + + var widget_uuid = 0, + widget_slice = Array.prototype.slice; + + $.cleanData = (function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; (elem = elems[i]) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + })( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; + }; + + $.widget.extend = function( target ) { + var input = widget_slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = widget_slice.call( arguments, 1 ), + returnValue = this; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; + }; + + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
          ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widget_uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + } + + return this; + }, + + enable: function() { + return this._setOptions({ disabled: false }); + }, + disable: function() { + return this._setOptions({ disabled: true }); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; + }); + + var widget = $.widget; + + + +})); diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js new file mode 100644 index 000000000..54457dab7 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js @@ -0,0 +1,1910 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7 + * Copyright (C) 2014 Oliver Nightingale + * MIT Licensed + * @license + */ + +(function(){ + + /** + * Convenience function for instantiating a new lunr index and configuring it + * with the default pipeline functions and the passed config function. + * + * When using this convenience function a new index will be created with the + * following functions already in the pipeline: + * + * lunr.StopWordFilter - filters out any stop words before they enter the + * index + * + * lunr.stemmer - stems the tokens before entering the index. + * + * Example: + * + * var idx = lunr(function () { + * this.field('title', 10) + * this.field('tags', 100) + * this.field('body') + * + * this.ref('cid') + * + * this.pipeline.add(function () { + * // some custom pipeline function + * }) + * + * }) + * + * @param {Function} config A function that will be called with the new instance + * of the lunr.Index as both its context and first parameter. It can be used to + * customize the instance of new lunr.Index. + * @namespace + * @module + * @returns {lunr.Index} + * + */ + var lunr = function (config) { + var idx = new lunr.Index + + idx.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + if (config) config.call(idx, idx) + + return idx + } + + lunr.version = "0.5.7" + /*! + * lunr.utils + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A namespace containing utils for the rest of the lunr library + */ + lunr.utils = {} + + /** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf Utils + */ + lunr.utils.warn = (function (global) { + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + })(this) + + /*! + * lunr.EventEmitter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers. + * + * @constructor + */ + lunr.EventEmitter = function () { + this.events = {} + } + + /** + * Binds a handler function to a specific event(s). + * + * Can bind a single function to many different events in one call. + * + * @param {String} [eventName] The name(s) of events to bind this function to. + * @param {Function} handler The function to call when an event is fired. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.addListener = function () { + var args = Array.prototype.slice.call(arguments), + fn = args.pop(), + names = args + + if (typeof fn !== "function") throw new TypeError ("last argument must be a function") + + names.forEach(function (name) { + if (!this.hasHandler(name)) this.events[name] = [] + this.events[name].push(fn) + }, this) + } + + /** + * Removes a handler function from a specific event. + * + * @param {String} eventName The name of the event to remove this function from. + * @param {Function} handler The function to remove from an event. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.removeListener = function (name, fn) { + if (!this.hasHandler(name)) return + + var fnIndex = this.events[name].indexOf(fn) + this.events[name].splice(fnIndex, 1) + + if (!this.events[name].length) delete this.events[name] + } + + /** + * Calls all functions bound to the given event. + * + * Additional data can be passed to the event handler as arguments to `emit` + * after the event name. + * + * @param {String} eventName The name of the event to emit. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.emit = function (name) { + if (!this.hasHandler(name)) return + + var args = Array.prototype.slice.call(arguments, 1) + + this.events[name].forEach(function (fn) { + fn.apply(undefined, args) + }) + } + + /** + * Checks whether a handler has ever been stored against an event. + * + * @param {String} eventName The name of the event to check. + * @private + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.hasHandler = function (name) { + return name in this.events + } + + /*! + * lunr.tokenizer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A function for splitting a string into tokens ready to be inserted into + * the search index. + * + * @module + * @param {String} obj The string to convert into tokens + * @returns {Array} + */ + lunr.tokenizer = function (obj) { + if (!arguments.length || obj == null || obj == undefined) return [] + if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() }) + + var str = obj.toString().replace(/^\s+/, '') + + for (var i = str.length - 1; i >= 0; i--) { + if (/\S/.test(str.charAt(i))) { + str = str.substring(0, i + 1) + break + } + } + + return str + .split(/(?:\s+|\-)/) + .filter(function (token) { + return !!token + }) + .map(function (token) { + return token.toLowerCase() + }) + } + /*! + * lunr.Pipeline + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ + lunr.Pipeline = function () { + this._stack = [] + } + + lunr.Pipeline.registeredFunctions = {} + + /** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {Function} fn The function to check for. + * @param {String} label The label to register this function with + * @memberOf Pipeline + */ + lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn + } + + /** + * Warns if the function is not registered as a Pipeline function. + * + * @param {Function} fn The function to check for. + * @private + * @memberOf Pipeline + */ + lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } + } + + /** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised The serialised pipeline to load. + * @returns {lunr.Pipeline} + * @memberOf Pipeline + */ + lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error ('Cannot load un-registered function: ' + fnName) + } + }) + + return pipeline + } + + /** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} functions Any number of functions to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) + } + + /** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + 1 + this._stack.splice(pos, 0, newFn) + } + + /** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + this._stack.splice(pos, 0, newFn) + } + + /** + * Removes a function from the pipeline. + * + * @param {Function} fn The function to remove from the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + this._stack.splice(pos, 1) + } + + /** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.run = function (tokens) { + var out = [], + tokenLength = tokens.length, + stackLength = this._stack.length + + for (var i = 0; i < tokenLength; i++) { + var token = tokens[i] + + for (var j = 0; j < stackLength; j++) { + token = this._stack[j](token, i, tokens) + if (token === void 0) break + }; + + if (token !== void 0) out.push(token) + }; + + return out + } + + /** + * Resets the pipeline by removing any existing processors. + * + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.reset = function () { + this._stack = [] + } + + /** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) + } + /*! + * lunr.Vector + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Vectors implement vector related operations for + * a series of elements. + * + * @constructor + */ + lunr.Vector = function () { + this._magnitude = null + this.list = undefined + this.length = 0 + } + + /** + * lunr.Vector.Node is a simple struct for each node + * in a lunr.Vector. + * + * @private + * @param {Number} The index of the node in the vector. + * @param {Object} The data at this node in the vector. + * @param {lunr.Vector.Node} The node directly after this node in the vector. + * @constructor + * @memberOf Vector + */ + lunr.Vector.Node = function (idx, val, next) { + this.idx = idx + this.val = val + this.next = next + } + + /** + * Inserts a new value at a position in a vector. + * + * @param {Number} The index at which to insert a value. + * @param {Object} The object to insert in the vector. + * @memberOf Vector. + */ + lunr.Vector.prototype.insert = function (idx, val) { + var list = this.list + + if (!list) { + this.list = new lunr.Vector.Node (idx, val, list) + return this.length++ + } + + var prev = list, + next = list.next + + while (next != undefined) { + if (idx < next.idx) { + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + prev = next, next = next.next + } + + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + /** + * Calculates the magnitude of this vector. + * + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.magnitude = function () { + if (this._magniture) return this._magnitude + var node = this.list, + sumOfSquares = 0, + val + + while (node) { + val = node.val + sumOfSquares += val * val + node = node.next + } + + return this._magnitude = Math.sqrt(sumOfSquares) + } + + /** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector The vector to compute the dot product with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.dot = function (otherVector) { + var node = this.list, + otherNode = otherVector.list, + dotProduct = 0 + + while (node && otherNode) { + if (node.idx < otherNode.idx) { + node = node.next + } else if (node.idx > otherNode.idx) { + otherNode = otherNode.next + } else { + dotProduct += node.val * otherNode.val + node = node.next + otherNode = otherNode.next + } + } + + return dotProduct + } + + /** + * Calculates the cosine similarity between this vector and another + * vector. + * + * @param {lunr.Vector} otherVector The other vector to calculate the + * similarity with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) + } + /*! + * lunr.SortedSet + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.SortedSets are used to maintain an array of uniq values in a sorted + * order. + * + * @constructor + */ + lunr.SortedSet = function () { + this.length = 0 + this.elements = [] + } + + /** + * Loads a previously serialised sorted set. + * + * @param {Array} serialisedData The serialised set to load. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.load = function (serialisedData) { + var set = new this + + set.elements = serialisedData + set.length = serialisedData.length + + return set + } + + /** + * Inserts new items into the set in the correct position to maintain the + * order. + * + * @param {Object} The objects to add to this set. + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.add = function () { + Array.prototype.slice.call(arguments).forEach(function (element) { + if (~this.indexOf(element)) return + this.elements.splice(this.locationFor(element), 0, element) + }, this) + + this.length = this.elements.length + } + + /** + * Converts this sorted set into an array. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toArray = function () { + return this.elements.slice() + } + + /** + * Creates a new array with the results of calling a provided function on every + * element in this sorted set. + * + * Delegates to Array.prototype.map and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * for the function fn. + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.map = function (fn, ctx) { + return this.elements.map(fn, ctx) + } + + /** + * Executes a provided function once per sorted set element. + * + * Delegates to Array.prototype.forEach and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * @memberOf SortedSet + * for the function fn. + */ + lunr.SortedSet.prototype.forEach = function (fn, ctx) { + return this.elements.forEach(fn, ctx) + } + + /** + * Returns the index at which a given element can be found in the + * sorted set, or -1 if it is not present. + * + * @param {Object} elem The object to locate in the sorted set. + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.indexOf = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem === elem) { + return pivot + } else { + return -1 + } + } + + if (pivotElem < elem) return this.indexOf(elem, pivot, end) + if (pivotElem > elem) return this.indexOf(elem, start, pivot) + if (pivotElem === elem) return pivot + } + + /** + * Returns the position within the sorted set that an element should be + * inserted at to maintain the current order of the set. + * + * This function assumes that the element to search for does not already exist + * in the sorted set. + * + * @param {Object} elem The elem to find the position for in the set + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.locationFor = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem > elem) return pivot + if (pivotElem < elem) return pivot + 1 + } + + if (pivotElem < elem) return this.locationFor(elem, pivot, end) + if (pivotElem > elem) return this.locationFor(elem, start, pivot) + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the intersection + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to intersect with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.intersect = function (otherSet) { + var intersectSet = new lunr.SortedSet, + i = 0, j = 0, + a_len = this.length, b_len = otherSet.length, + a = this.elements, b = otherSet.elements + + while (true) { + if (i > a_len - 1 || j > b_len - 1) break + + if (a[i] === b[j]) { + intersectSet.add(a[i]) + i++, j++ + continue + } + + if (a[i] < b[j]) { + i++ + continue + } + + if (a[i] > b[j]) { + j++ + continue + } + }; + + return intersectSet + } + + /** + * Makes a copy of this set + * + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.clone = function () { + var clone = new lunr.SortedSet + + clone.elements = this.toArray() + clone.length = clone.elements.length + + return clone + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the union + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to union with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.union = function (otherSet) { + var longSet, shortSet, unionSet + + if (this.length >= otherSet.length) { + longSet = this, shortSet = otherSet + } else { + longSet = otherSet, shortSet = this + } + + unionSet = longSet.clone() + + unionSet.add.apply(unionSet, shortSet.toArray()) + + return unionSet + } + + /** + * Returns a representation of the sorted set ready for serialisation. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toJSON = function () { + return this.toArray() + } + /*! + * lunr.Index + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Index is object that manages a search index. It contains the indexes + * and stores all the tokens and document lookups. It also provides the main + * user facing API for the library. + * + * @constructor + */ + lunr.Index = function () { + this._fields = [] + this._ref = 'id' + this.pipeline = new lunr.Pipeline + this.documentStore = new lunr.Store + this.tokenStore = new lunr.TokenStore + this.corpusTokens = new lunr.SortedSet + this.eventEmitter = new lunr.EventEmitter + + this._idfCache = {} + + this.on('add', 'remove', 'update', (function () { + this._idfCache = {} + }).bind(this)) + } + + /** + * Bind a handler to events being emitted by the index. + * + * The handler can be bound to many events at the same time. + * + * @param {String} [eventName] The name(s) of events to bind the function to. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.on = function () { + var args = Array.prototype.slice.call(arguments) + return this.eventEmitter.addListener.apply(this.eventEmitter, args) + } + + /** + * Removes a handler from an event being emitted by the index. + * + * @param {String} eventName The name of events to remove the function from. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.off = function (name, fn) { + return this.eventEmitter.removeListener(name, fn) + } + + /** + * Loads a previously serialised index. + * + * Issues a warning if the index being imported was serialised + * by a different version of lunr. + * + * @param {Object} serialisedData The serialised set to load. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.load = function (serialisedData) { + if (serialisedData.version !== lunr.version) { + lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) + } + + var idx = new this + + idx._fields = serialisedData.fields + idx._ref = serialisedData.ref + + idx.documentStore = lunr.Store.load(serialisedData.documentStore) + idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) + idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) + idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) + + return idx + } + + /** + * Adds a field to the list of fields that will be searchable within documents + * in the index. + * + * An optional boost param can be passed to affect how much tokens in this field + * rank in search results, by default the boost value is 1. + * + * Fields should be added before any documents are added to the index, fields + * that are added after documents are added to the index will only apply to new + * documents added to the index. + * + * @param {String} fieldName The name of the field within the document that + * should be indexed + * @param {Number} boost An optional boost that can be applied to terms in this + * field. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.field = function (fieldName, opts) { + var opts = opts || {}, + field = { name: fieldName, boost: opts.boost || 1 } + + this._fields.push(field) + return this + } + + /** + * Sets the property used to uniquely identify documents added to the index, + * by default this property is 'id'. + * + * This should only be changed before adding documents to the index, changing + * the ref property without resetting the index can lead to unexpected results. + * + * @param {String} refName The property to use to uniquely identify the + * documents in the index. + * @param {Boolean} emitEvent Whether to emit add events, defaults to true + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.ref = function (refName) { + this._ref = refName + return this + } + + /** + * Add a document to the index. + * + * This is the way new documents enter the index, this function will run the + * fields from the document through the index's pipeline and then add it to + * the index, it will then show up in search results. + * + * An 'add' event is emitted with the document that has been added and the index + * the document has been added to. This event can be silenced by passing false + * as the second argument to add. + * + * @param {Object} doc The document to add to the index. + * @param {Boolean} emitEvent Whether or not to emit events, default true. + * @memberOf Index + */ + lunr.Index.prototype.add = function (doc, emitEvent) { + var docTokens = {}, + allDocumentTokens = new lunr.SortedSet, + docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + this._fields.forEach(function (field) { + var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) + + docTokens[field.name] = fieldTokens + lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) + }, this) + + this.documentStore.set(docRef, allDocumentTokens) + lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) + + for (var i = 0; i < allDocumentTokens.length; i++) { + var token = allDocumentTokens.elements[i] + var tf = this._fields.reduce(function (memo, field) { + var fieldLength = docTokens[field.name].length + + if (!fieldLength) return memo + + var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length + + return memo + (tokenCount / fieldLength * field.boost) + }, 0) + + this.tokenStore.add(token, { ref: docRef, tf: tf }) + }; + + if (emitEvent) this.eventEmitter.emit('add', doc, this) + } + + /** + * Removes a document from the index. + * + * To make sure documents no longer show up in search results they can be + * removed from the index using this method. + * + * The document passed only needs to have the same ref property value as the + * document that was added to the index, they could be completely different + * objects. + * + * A 'remove' event is emitted with the document that has been removed and the index + * the document has been removed from. This event can be silenced by passing false + * as the second argument to remove. + * + * @param {Object} doc The document to remove from the index. + * @param {Boolean} emitEvent Whether to emit remove events, defaults to true + * @memberOf Index + */ + lunr.Index.prototype.remove = function (doc, emitEvent) { + var docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + if (!this.documentStore.has(docRef)) return + + var docTokens = this.documentStore.get(docRef) + + this.documentStore.remove(docRef) + + docTokens.forEach(function (token) { + this.tokenStore.remove(token, docRef) + }, this) + + if (emitEvent) this.eventEmitter.emit('remove', doc, this) + } + + /** + * Updates a document in the index. + * + * When a document contained within the index gets updated, fields changed, + * added or removed, to make sure it correctly matched against search queries, + * it should be updated in the index. + * + * This method is just a wrapper around `remove` and `add` + * + * An 'update' event is emitted with the document that has been updated and the index. + * This event can be silenced by passing false as the second argument to update. Only + * an update event will be fired, the 'add' and 'remove' events of the underlying calls + * are silenced. + * + * @param {Object} doc The document to update in the index. + * @param {Boolean} emitEvent Whether to emit update events, defaults to true + * @see Index.prototype.remove + * @see Index.prototype.add + * @memberOf Index + */ + lunr.Index.prototype.update = function (doc, emitEvent) { + var emitEvent = emitEvent === undefined ? true : emitEvent + + this.remove(doc, false) + this.add(doc, false) + + if (emitEvent) this.eventEmitter.emit('update', doc, this) + } + + /** + * Calculates the inverse document frequency for a token within the index. + * + * @param {String} token The token to calculate the idf of. + * @see Index.prototype.idf + * @private + * @memberOf Index + */ + lunr.Index.prototype.idf = function (term) { + var cacheKey = "@" + term + if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey] + + var documentFrequency = this.tokenStore.count(term), + idf = 1 + + if (documentFrequency > 0) { + idf = 1 + Math.log(this.tokenStore.length / documentFrequency) + } + + return this._idfCache[cacheKey] = idf + } + + /** + * Searches the index using the passed query. + * + * Queries should be a string, multiple words are allowed and will lead to an + * AND based query, e.g. `idx.search('foo bar')` will run a search for + * documents containing both 'foo' and 'bar'. + * + * All query tokens are passed through the same pipeline that document tokens + * are passed through, so any language processing involved will be run on every + * query term. + * + * Each query term is expanded, so that the term 'he' might be expanded to + * 'hello' and 'help' if those terms were already included in the index. + * + * Matching documents are returned as an array of objects, each object contains + * the matching document ref, as set for this index, and the similarity score + * for this document against the query. + * + * @param {String} query The query to search the index with. + * @returns {Object} + * @see Index.prototype.idf + * @see Index.prototype.documentVector + * @memberOf Index + */ + lunr.Index.prototype.search = function (query) { + var queryTokens = this.pipeline.run(lunr.tokenizer(query)), + queryVector = new lunr.Vector, + documentSets = [], + fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) + + var hasSomeToken = queryTokens.some(function (token) { + return this.tokenStore.has(token) + }, this) + + if (!hasSomeToken) return [] + + queryTokens + .forEach(function (token, i, tokens) { + var tf = 1 / tokens.length * this._fields.length * fieldBoosts, + self = this + + var set = this.tokenStore.expand(token).reduce(function (memo, key) { + var pos = self.corpusTokens.indexOf(key), + idf = self.idf(key), + similarityBoost = 1, + set = new lunr.SortedSet + + // if the expanded key is not an exact match to the token then + // penalise the score for this key by how different the key is + // to the token. + if (key !== token) { + var diff = Math.max(3, key.length - token.length) + similarityBoost = 1 / Math.log(diff) + } + + // calculate the query tf-idf score for this token + // applying an similarityBoost to ensure exact matches + // these rank higher than expanded terms + if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost) + + // add all the documents that have this key into a set + Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) + + return memo.union(set) + }, new lunr.SortedSet) + + documentSets.push(set) + }, this) + + var documentSet = documentSets.reduce(function (memo, set) { + return memo.intersect(set) + }) + + return documentSet + .map(function (ref) { + return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } + }, this) + .sort(function (a, b) { + return b.score - a.score + }) + } + + /** + * Generates a vector containing all the tokens in the document matching the + * passed documentRef. + * + * The vector contains the tf-idf score for each token contained in the + * document with the passed documentRef. The vector will contain an element + * for every token in the indexes corpus, if the document does not contain that + * token the element will be 0. + * + * @param {Object} documentRef The ref to find the document with. + * @returns {lunr.Vector} + * @private + * @memberOf Index + */ + lunr.Index.prototype.documentVector = function (documentRef) { + var documentTokens = this.documentStore.get(documentRef), + documentTokensLength = documentTokens.length, + documentVector = new lunr.Vector + + for (var i = 0; i < documentTokensLength; i++) { + var token = documentTokens.elements[i], + tf = this.tokenStore.get(token)[documentRef].tf, + idf = this.idf(token) + + documentVector.insert(this.corpusTokens.indexOf(token), tf * idf) + }; + + return documentVector + } + + /** + * Returns a representation of the index ready for serialisation. + * + * @returns {Object} + * @memberOf Index + */ + lunr.Index.prototype.toJSON = function () { + return { + version: lunr.version, + fields: this._fields, + ref: this._ref, + documentStore: this.documentStore.toJSON(), + tokenStore: this.tokenStore.toJSON(), + corpusTokens: this.corpusTokens.toJSON(), + pipeline: this.pipeline.toJSON() + } + } + + /** + * Applies a plugin to the current index. + * + * A plugin is a function that is called with the index as its context. + * Plugins can be used to customise or extend the behaviour the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied to the index. + * + * The plugin function will be called with the index as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index as its context. + * + * Example: + * + * var myPlugin = function (idx, arg1, arg2) { + * // `this` is the index to be extended + * // apply any extensions etc here. + * } + * + * var idx = lunr(function () { + * this.use(myPlugin, 'arg1', 'arg2') + * }) + * + * @param {Function} plugin The plugin to apply. + * @memberOf Index + */ + lunr.Index.prototype.use = function (plugin) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + plugin.apply(this, args) + } + /*! + * lunr.Store + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Store is a simple key-value store used for storing sets of tokens for + * documents stored in index. + * + * @constructor + * @module + */ + lunr.Store = function () { + this.store = {} + this.length = 0 + } + + /** + * Loads a previously serialised store + * + * @param {Object} serialisedData The serialised store to load. + * @returns {lunr.Store} + * @memberOf Store + */ + lunr.Store.load = function (serialisedData) { + var store = new this + + store.length = serialisedData.length + store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { + memo[key] = lunr.SortedSet.load(serialisedData.store[key]) + return memo + }, {}) + + return store + } + + /** + * Stores the given tokens in the store against the given id. + * + * @param {Object} id The key used to store the tokens against. + * @param {Object} tokens The tokens to store against the key. + * @memberOf Store + */ + lunr.Store.prototype.set = function (id, tokens) { + if (!this.has(id)) this.length++ + this.store[id] = tokens + } + + /** + * Retrieves the tokens from the store for a given key. + * + * @param {Object} id The key to lookup and retrieve from the store. + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.get = function (id) { + return this.store[id] + } + + /** + * Checks whether the store contains a key. + * + * @param {Object} id The id to look up in the store. + * @returns {Boolean} + * @memberOf Store + */ + lunr.Store.prototype.has = function (id) { + return id in this.store + } + + /** + * Removes the value for a key in the store. + * + * @param {Object} id The id to remove from the store. + * @memberOf Store + */ + lunr.Store.prototype.remove = function (id) { + if (!this.has(id)) return + + delete this.store[id] + this.length-- + } + + /** + * Returns a representation of the store ready for serialisation. + * + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.toJSON = function () { + return { + store: this.store, + length: this.length + } + } + + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartaurs.org/~martin + * + * @module + * @param {String} str The string to stem + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return porterStemmer; + })(); + + lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') + /*! + * lunr.stopWordFilter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stopWordFilter = function (token) { + if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token + } + + lunr.stopWordFilter.stopWords = new lunr.SortedSet + lunr.stopWordFilter.stopWords.length = 119 + lunr.stopWordFilter.stopWords.elements = [ + "", + "a", + "able", + "about", + "across", + "after", + "all", + "almost", + "also", + "am", + "among", + "an", + "and", + "any", + "are", + "as", + "at", + "be", + "because", + "been", + "but", + "by", + "can", + "cannot", + "could", + "dear", + "did", + "do", + "does", + "either", + "else", + "ever", + "every", + "for", + "from", + "get", + "got", + "had", + "has", + "have", + "he", + "her", + "hers", + "him", + "his", + "how", + "however", + "i", + "if", + "in", + "into", + "is", + "it", + "its", + "just", + "least", + "let", + "like", + "likely", + "may", + "me", + "might", + "most", + "must", + "my", + "neither", + "no", + "nor", + "not", + "of", + "off", + "often", + "on", + "only", + "or", + "other", + "our", + "own", + "rather", + "said", + "say", + "says", + "she", + "should", + "since", + "so", + "some", + "than", + "that", + "the", + "their", + "them", + "then", + "there", + "these", + "they", + "this", + "tis", + "to", + "too", + "twas", + "us", + "wants", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "would", + "yet", + "you", + "your" + ] + + lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') + /*! + * lunr.trimmer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the begining and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.trimmer = function (token) { + return token + .replace(/^\W+/, '') + .replace(/\W+$/, '') + } + + lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.TokenStore is used for efficient storing and lookup of the reverse + * index of token to document ref. + * + * @constructor + */ + lunr.TokenStore = function () { + this.root = { docs: {} } + this.length = 0 + } + + /** + * Loads a previously serialised token store + * + * @param {Object} serialisedData The serialised token store to load. + * @returns {lunr.TokenStore} + * @memberOf TokenStore + */ + lunr.TokenStore.load = function (serialisedData) { + var store = new this + + store.root = serialisedData.root + store.length = serialisedData.length + + return store + } + + /** + * Adds a new token doc pair to the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to store the doc under + * @param {Object} doc The doc to store against the token + * @param {Object} root An optional node at which to start looking for the + * correct place to enter the doc, by default the root of this lunr.TokenStore + * is used. + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.add = function (token, doc, root) { + var root = root || this.root, + key = token[0], + rest = token.slice(1) + + if (!(key in root)) root[key] = {docs: {}} + + if (rest.length === 0) { + root[key].docs[doc.ref] = doc + this.length += 1 + return + } else { + return this.add(rest, doc, root[key]) + } + } + + /** + * Checks whether this key is contained within this lunr.TokenStore. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to check for + * @param {Object} root An optional node at which to start + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.has = function (token) { + if (!token) return false + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return false + + node = node[token[i]] + } + + return true + } + + /** + * Retrieve a node from the token store for a given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the node for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @see TokenStore.prototype.get + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.getNode = function (token) { + if (!token) return {} + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return {} + + node = node[token[i]] + } + + return node + } + + /** + * Retrieve the documents for a node for the given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.get = function (token, root) { + return this.getNode(token, root).docs || {} + } + + lunr.TokenStore.prototype.count = function (token, root) { + return Object.keys(this.get(token, root)).length + } + + /** + * Remove the document identified by ref from the token in the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {String} ref The ref of the document to remove from this token. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.remove = function (token, ref) { + if (!token) return + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!(token[i] in node)) return + node = node[token[i]] + } + + delete node.docs[ref] + } + + /** + * Find all the possible suffixes of the passed token using tokens + * currently in the store. + * + * @param {String} token The token to expand. + * @returns {Array} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.expand = function (token, memo) { + var root = this.getNode(token), + docs = root.docs || {}, + memo = memo || [] + + if (Object.keys(docs).length) memo.push(token) + + Object.keys(root) + .forEach(function (key) { + if (key === 'docs') return + + memo.concat(this.expand(token + key, memo)) + }, this) + + return memo + } + + /** + * Returns a representation of the token store ready for serialisation. + * + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.toJSON = function () { + return { + root: this.root, + length: this.length + } + } + + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})() diff --git a/samples/rest-notes-slate/slate/source/layouts/layout.erb b/samples/rest-notes-slate/slate/source/layouts/layout.erb new file mode 100644 index 000000000..36ae0f95f --- /dev/null +++ b/samples/rest-notes-slate/slate/source/layouts/layout.erb @@ -0,0 +1,102 @@ +<%# +Copyright 2008-2013 Concur Technologies, Inc. + +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. +%> +<% language_tabs = current_page.data.language_tabs %> + + + + + + + <%= current_page.data.title || "API Documentation" %> + + <%= stylesheet_link_tag :screen, media: :screen %> + <%= stylesheet_link_tag :print, media: :print %> + + <% if current_page.data.search %> + <%= javascript_include_tag "all" %> + <% else %> + <%= javascript_include_tag "all_nosearch" %> + <% end %> + + <% if language_tabs %> + + <% end %> + + + + + + NAV + <%= image_tag('navbar.png') %> + + +
          + <%= image_tag "logo.png" %> + <% if language_tabs %> +
          + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
          + <% end %> + <% if current_page.data.search %> + +
            + <% end %> +
            +
            + <% if current_page.data.toc_footers %> + + <% end %> +
            +
            +
            +
            + <%= yield %> + <% current_page.data.includes && current_page.data.includes.each do |include| %> + <%= partial "includes/#{include}" %> + <% end %> +
            +
            + <% if language_tabs %> +
            + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
            + <% end %> +
            +
            + + diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss b/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss new file mode 100644 index 000000000..b59948398 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss @@ -0,0 +1,38 @@ +@font-face { + font-family: 'slate'; + src:font-url('slate.eot?-syv14m'); + src:font-url('slate.eot?#iefix-syv14m') format('embedded-opentype'), + font-url('slate.woff2?-syv14m') format('woff2'), + font-url('slate.woff?-syv14m') format('woff'), + font-url('slate.ttf?-syv14m') format('truetype'), + font-url('slate.svg?-syv14m#slate') format('svg'); + font-weight: normal; + font-style: normal; +} + +%icon { + font-family: 'slate'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; +} + +%icon-exclamation-sign { + @extend %icon; + content: "\e600"; +} +%icon-info-sign { + @extend %icon; + content: "\e602"; +} +%icon-ok-sign { + @extend %icon; + content: "\e606"; +} +%icon-search { + @extend %icon; + content: "\e607"; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css b/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css new file mode 100644 index 000000000..46f646a5c --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb b/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb new file mode 100644 index 000000000..dfeb0c152 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb @@ -0,0 +1,27 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +@import 'variables'; + +<%= Rouge::Themes::Base16::Monokai.render(:scope => '.highlight') %> + +.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs { + color: #909090; +} + +.highlight, .highlight .w { + background-color: $code-bg; +} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss b/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss new file mode 100644 index 000000000..5fe64b1f3 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss @@ -0,0 +1,109 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + + +//////////////////////////////////////////////////////////////////////////////// +// CUSTOMIZE SLATE +//////////////////////////////////////////////////////////////////////////////// +// Use these settings to help adjust the appearance of Slate + + +// BACKGROUND COLORS +//////////////////// +$nav-bg: #393939; +$examples-bg: #393939; +$code-bg: #292929; +$code-annotation-bg: #1c1c1c; +$nav-subitem-bg: #262626; +$nav-active-bg: #2467af; +$lang-select-border: #000; +$lang-select-bg: #222; +$lang-select-active-bg: $examples-bg; // feel free to change this to blue or something +$lang-select-pressed-bg: #111; // color of language tab bg when mouse is pressed +$main-bg: #eaf2f6; +$aside-notice-bg: #8fbcd4; +$aside-warning-bg: #c97a7e; +$aside-success-bg: #6ac174; +$search-notice-bg: #c97a7e; + + +// TEXT COLORS +//////////////////// +$main-text: #333; // main content text color +$nav-text: #fff; +$nav-active-text: #fff; +$lang-select-text: #fff; // color of unselected language tab text +$lang-select-active-text: #fff; // color of selected language tab text +$lang-select-pressed-text: #fff; // color of language tab text when mouse is pressed + + +// SIZES +//////////////////// +$nav-width: 230px; // width of the navbar +$examples-width: 50%; // portion of the screen taken up by code examples +$logo-margin: 20px; // margin between nav items and logo, ignored if search is active +$main-padding: 28px; // padding to left and right of content & examples +$nav-padding: 15px; // padding to left and right of navbar +$nav-v-padding: 10px; // padding used vertically around search boxes and results +$nav-indent: 10px; // extra padding for ToC subitems +$code-annotation-padding: 13px; // padding inside code annotations +$h1-margin-bottom: 21px; // padding under the largest header tags +$tablet-width: 930px; // min width before reverting to tablet size +$phone-width: $tablet-width - $nav-width; // min width before reverting to mobile size + + +// FONTS +//////////////////// +%default-font { + font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei","微软雅黑", STXihei, "华文细黑", sans-serif; + font-size: 13px; +} + +%header-font { + @extend %default-font; + font-weight: bold; +} + +%code-font { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif; + font-size: 12px; + line-height: 1.5; +} + + +// OTHER +//////////////////// +$nav-active-shadow: #000; +$nav-footer-border-color: #666; +$nav-embossed-border-top: #000; +$nav-embossed-border-bottom: #939393; +$main-embossed-text-shadow: 0px 1px 0px #fff; +$search-box-border-color: #666; + + +//////////////////////////////////////////////////////////////////////////////// +// INTERNAL +//////////////////////////////////////////////////////////////////////////////// +// These settings are probably best left alone. + +%break-words { + word-break: break-all; + + /* Non standard for webkit */ + word-break: break-word; + + hyphens: auto; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss new file mode 100644 index 000000000..4bda057f0 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss @@ -0,0 +1,142 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'compass'; +@import 'variables'; +@import 'icon-font'; + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +$print-color: #999; +$print-color-light: #ccc; +$print-font-size: 12px; + +body { + @extend %default-font; +} + +.tocify, .toc-footer, .lang-selector, .search, #nav-button { + display: none; +} + +.tocify-wrapper>img { + margin: 0 auto; + display: block; +} + +.content { + font-size: 12px; + + pre, code { + @extend %code-font; + @extend %break-words; + border: 1px solid $print-color; + border-radius: 5px; + font-size: 0.8em; + } + + pre { + padding: 1.3em; + } + + code { + padding: 0.2em; + } + + table { + border: 1px solid $print-color; + tr { + border-bottom: 1px solid $print-color; + } + td,th { + padding: 0.7em; + } + } + + p { + line-height: 1.5; + } + + a { + text-decoration: none; + color: #000; + } + + h1 { + @extend %header-font; + font-size: 2.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + margin-top: 1em; + margin-bottom: $h1-margin-bottom; + border: 2px solid $print-color-light; + border-width: 2px 0; + text-align: center; + } + + h2 { + @extend %header-font; + font-size: 1.8em; + margin-top: 2em; + border-top: 2px solid $print-color-light; + padding-top: 0.8em; + } + + h1+h2, h1+div+h2 { + border-top: none; + padding-top: 0; + margin-top: 0; + } + + h3, h4 { + @extend %header-font; + font-size: 0.8em; + margin-top: 1.5em; + margin-bottom: 0.8em; + text-transform: uppercase; + } + + h5, h6 { + text-transform: uppercase; + } + + aside { + padding: 1em; + border: 1px solid $print-color-light; + border-radius: 5px; + margin-top: 1.5em; + margin-bottom: 1.5em; + line-height: 1.6; + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } +} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss new file mode 100644 index 000000000..e4b3ef82b --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss @@ -0,0 +1,620 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'compass'; +@import 'variables'; +@import 'syntax'; +@import 'icon-font'; + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// GENERAL STUFF +//////////////////////////////////////////////////////////////////////////////// + +html, body { + color: $main-text; + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + @extend %default-font; + background-color: $main-bg; + height: 100%; + -webkit-text-size-adjust: none; /* Never autoresize text */ +} + +//////////////////////////////////////////////////////////////////////////////// +// TABLE OF CONTENTS +//////////////////////////////////////////////////////////////////////////////// + +#toc > ul > li > a > span { + float: right; + background-color: #2484FF; + border-radius: 40px; + width: 20px; +} + +@mixin embossed-bg { + background: + linear-gradient(to bottom, rgba(#000, 0.2), rgba(#000, 0) 8px), + linear-gradient(to top, rgba(#000, 0.2), rgba(#000, 0) 8px), + linear-gradient(to bottom, rgba($nav-embossed-border-top, 1), rgba($nav-embossed-border-top, 0) 1.5px), + linear-gradient(to top, rgba($nav-embossed-border-bottom, 1), rgba($nav-embossed-border-bottom, 0) 1.5px), + $nav-subitem-bg; +} + +.tocify-wrapper { + transition: left 0.3s ease-in-out; + + overflow-y: auto; + overflow-x: hidden; + position: fixed; + z-index: 30; + top: 0; + left: 0; + bottom: 0; + width: $nav-width; + background-color: $nav-bg; + font-size: 13px; + font-weight: bold; + + // language selector for mobile devices + .lang-selector { + display: none; + a { + padding-top: 0.5em; + padding-bottom: 0.5em; + } + } + + // This is the logo at the top of the ToC + &>img { + display: block; + } + + &>.search { + position: relative; + + input { + background: $nav-bg; + border-width: 0 0 1px 0; + border-color: $search-box-border-color; + padding: 6px 0 6px 20px; + box-sizing: border-box; + margin: $nav-v-padding $nav-padding; + width: $nav-width - 30; + outline: none; + color: $nav-text; + border-radius: 0; /* ios has a default border radius */ + } + + &:before { + position: absolute; + top: 17px; + left: $nav-padding; + color: $nav-text; + @extend %icon-search; + } + } + + img+.tocify { + margin-top: $logo-margin; + } + + .search-results { + margin-top: 0; + box-sizing: border-box; + height: 0; + overflow-y: auto; + overflow-x: hidden; + transition-property: height, margin; + transition-duration: 180ms; + transition-timing-function: ease-in-out; + &.visible { + height: 30%; + margin-bottom: 1em; + } + + @include embossed-bg; + + li { + margin: 1em $nav-padding; + line-height: 1; + } + + a { + color: $nav-text; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + + .tocify-item>a, .toc-footer li { + padding: 0 $nav-padding 0 $nav-padding; + display: block; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + // The Table of Contents is composed of multiple nested + // unordered lists. These styles remove the default + // styling of an unordered list because it is ugly. + ul, li { + list-style: none; + margin: 0; + padding: 0; + line-height: 28px; + } + + li { + color: $nav-text; + transition-property: background; + transition-timing-function: linear; + transition-duration: 230ms; + } + + // This is the currently selected ToC entry + .tocify-focus { + box-shadow: 0px 1px 0px $nav-active-shadow; + background-color: $nav-active-bg; + color: $nav-active-text; + } + + // Subheaders are the submenus that slide open + // in the table of contents. + .tocify-subheader { + display: none; // tocify will override this when needed + background-color: $nav-subitem-bg; + font-weight: 500; + .tocify-item>a { + padding-left: $nav-padding + $nav-indent; + font-size: 12px; + } + + // for embossed look: + @include embossed-bg; + &>li:last-child { + box-shadow: none; // otherwise it'll overflow out of the subheader + } + } + + .toc-footer { + padding: 1em 0; + margin-top: 1em; + border-top: 1px dashed $nav-footer-border-color; + + li,a { + color: $nav-text; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + li { + font-size: 0.8em; + line-height: 1.7; + text-decoration: none; + } + } + +} + +// button to show navigation on mobile devices +#nav-button { + span { + display: block; + $side-pad: $main-padding / 2 - 8px; + padding: $side-pad $side-pad $side-pad; + background-color: rgba($main-bg, 0.7); + transform-origin: 0 0; + transform: rotate(-90deg) translate(-100%, 0); + border-radius: 0 0 0 5px; + } + padding: 0 1.5em 5em 0; // increase touch size area + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 100; + color: #000; + text-decoration: none; + font-weight: bold; + opacity: 0.7; + line-height: 16px; + img { + height: 16px; + vertical-align: bottom; + } + + transition: left 0.3s ease-in-out; + + &:hover { opacity: 1; } + &.open {left: $nav-width} +} + + +//////////////////////////////////////////////////////////////////////////////// +// PAGE LAYOUT AND CODE SAMPLE BACKGROUND +//////////////////////////////////////////////////////////////////////////////// + +.page-wrapper { + margin-left: $nav-width; + position: relative; + z-index: 10; + background-color: $main-bg; + min-height: 100%; + + padding-bottom: 1px; // prevent margin overflow + + // The dark box is what gives the code samples their dark background. + // It sits essentially under the actual content block, which has a + // transparent background. + // I know, it's hackish, but it's the simplist way to make the left + // half of the content always this background color. + .dark-box { + width: $examples-width; + background-color: $examples-bg; + position: absolute; + right: 0; + top: 0; + bottom: 0; + } + + .lang-selector { + position: fixed; + z-index: 50; + border-bottom: 5px solid $lang-select-active-bg; + } +} + +.lang-selector { + background-color: $lang-select-bg; + width: 100%; + font-weight: bold; + a { + display: block; + float:left; + color: $lang-select-text; + text-decoration: none; + padding: 0 10px; + line-height: 30px; + outline: 0; + + &:active, &:focus { + background-color: $lang-select-pressed-bg; + color: $lang-select-pressed-text; + } + + &.active { + background-color: $lang-select-active-bg; + color: $lang-select-active-text; + } + } + + &:after { + content: ''; + clear: both; + display: block; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CONTENT STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff with the light background in the left half of the page + +.content { + // to place content above the dark box + position: relative; + z-index: 30; + + &:after { + content: ''; + display: block; + clear: both; + } + + &>h1, &>h2, &>h3, &>h4, &>h5, &>h6, &>p, &>table, &>ul, &>ol, &>aside, &>dl { + margin-right: $examples-width; + padding: 0 $main-padding; + box-sizing: border-box; + display: block; + @include text-shadow($main-embossed-text-shadow); + + @extend %left-col; + } + + &>ul, &>ol { + padding-left: $main-padding + 15px; + } + + // the div is the tocify hidden div for placeholding stuff + &>h1, &>h2, &>div { + clear:both; + } + + h1 { + @extend %header-font; + font-size: 30px; + padding-top: 0.5em; + padding-bottom: 0.5em; + border-bottom: 1px solid #ccc; + margin-bottom: $h1-margin-bottom; + margin-top: 2em; + border-top: 1px solid #ddd; + background-image: linear-gradient(to bottom, #fff, #f9f9f9); + } + + h1:first-child, div:first-child + h1 { + border-top-width: 0; + margin-top: 0; + } + + h2 { + @extend %header-font; + font-size: 20px; + margin-top: 4em; + margin-bottom: 0; + border-top: 1px solid #ccc; + padding-top: 1.2em; + padding-bottom: 1.2em; + background-image: linear-gradient(to bottom, rgba(#fff, 0.4), rgba(#fff, 0)); + } + + // h2s right after h1s should bump right up + // against the h1s. + h1 + h2, h1 + div + h2 { + margin-top: $h1-margin-bottom * -1; + border-top: none; + } + + h3, h4, h5, h6 { + @extend %header-font; + font-size: 15px; + margin-top: 2.5em; + margin-bottom: 0.8em; + } + + h4, h5, h6 { + font-size: 10px; + } + + hr { + margin: 2em 0; + border-top: 2px solid $examples-bg; + border-bottom: 2px solid $main-bg; + } + + table { + margin-bottom: 1em; + overflow: auto; + th,td { + text-align: left; + vertical-align: top; + line-height: 1.6; + } + + th { + padding: 5px 10px; + border-bottom: 1px solid #ccc; + vertical-align: bottom; + } + + td { + padding: 10px; + } + + tr:last-child { + border-bottom: 1px solid #ccc; + } + + tr:nth-child(odd)>td { + background-color: lighten($main-bg,4.2%); + } + + tr:nth-child(even)>td { + background-color: lighten($main-bg,2.4%); + } + } + + dt { + font-weight: bold; + } + + dd { + margin-left: 15px; + } + + p, li, dt, dd { + line-height: 1.6; + margin-top: 0; + } + + img { + max-width: 100%; + } + + code { + background-color: rgba(0,0,0,0.05); + padding: 3px; + border-radius: 3px; + @extend %break-words; + @extend %code-font; + } + + pre>code { + background-color: transparent; + padding: 0; + } + + aside { + padding-top: 1em; + padding-bottom: 1em; + @include text-shadow(0 1px 0 lighten($aside-notice-bg, 15%)); + margin-top: 1.5em; + margin-bottom: 1.5em; + background: $aside-notice-bg; + line-height: 1.6; + + &.warning { + background-color: $aside-warning-bg; + @include text-shadow(0 1px 0 lighten($aside-warning-bg, 15%)); + } + + &.success { + background-color: $aside-success-bg; + @include text-shadow(0 1px 0 lighten($aside-success-bg, 15%)); + } + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } + + .search-highlight { + padding: 2px; + margin: -2px; + border-radius: 4px; + border: 1px solid #F7E633; + @include text-shadow(1px 1px 0 #666); + background: linear-gradient(to top left, #F7E633 0%, #F1D32F 100%); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CODE SAMPLE STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff that appears in the right half of the page + +.content { + pre, blockquote { + background-color: $code-bg; + color: #fff; + + padding: 2em $main-padding; + margin: 0; + width: $examples-width; + + float:right; + clear:right; + + box-sizing: border-box; + @include text-shadow(0px 1px 2px rgba(0,0,0,0.4)); + + @extend %right-col; + + &>p { margin: 0; } + + a { + color: #fff; + text-decoration: none; + border-bottom: dashed 1px #ccc; + } + } + + pre { + @extend %code-font; + } + + blockquote { + &>p { + background-color: $code-annotation-bg; + border-radius: 5px; + padding: $code-annotation-padding; + color: #ccc; + border-top: 1px solid #000; + border-bottom: 1px solid #404040; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// RESPONSIVE DESIGN +//////////////////////////////////////////////////////////////////////////////// +// These are the styles for phones and tablets +// There are also a couple styles disperesed + +@media (max-width: $tablet-width) { + .tocify-wrapper { + left: -$nav-width; + + &.open { + left: 0; + } + } + + .page-wrapper { + margin-left: 0; + } + + #nav-button { + display: block; + } + + .tocify-wrapper .tocify-item > a { + padding-top: 0.3em; + padding-bottom: 0.3em; + } +} + +@media (max-width: $phone-width) { + .dark-box { + display: none; + } + + %left-col { + margin-right: 0; + } + + .tocify-wrapper .lang-selector { + display: block; + } + + .page-wrapper .lang-selector { + display: none; + } + + %right-col { + width: auto; + float: none; + } + + %right-col + %left-col { + margin-top: $main-padding; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java new file mode 100644 index 000000000..b32d0a119 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +public class Note { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String title; + + private String body; + + @ManyToMany + private List tags; + + @JsonIgnore + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java new file mode 100644 index 000000000..0325c49aa --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import org.springframework.data.repository.CrudRepository; + +public interface NoteRepository extends CrudRepository { + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java new file mode 100644 index 000000000..32c5ce243 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@EnableAutoConfiguration +@ComponentScan +@EnableJpaRepositories +public class RestNotesSlate { + + public static void main(String[] args) { + SpringApplication.run(RestNotesSlate.class, args); + } + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java new file mode 100644 index 000000000..970b642f4 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 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 com.example.notes; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +public class Tag { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String name; + + @ManyToMany(mappedBy = "tags") + private List notes; + + @JsonIgnore + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getNotes() { + return notes; + } + + public void setNotes(List notes) { + this.notes = notes; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java new file mode 100644 index 000000000..8790619bb --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014 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 com.example.notes; + +import org.springframework.data.repository.CrudRepository; + +public interface TagRepository extends CrudRepository { + +} diff --git a/samples/rest-notes-slate/src/main/resources/application.properties b/samples/rest-notes-slate/src/main/resources/application.properties new file mode 100644 index 000000000..8e06a8284 --- /dev/null +++ b/samples/rest-notes-slate/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java new file mode 100644 index 000000000..d4ea4ca06 --- /dev/null +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -0,0 +1,343 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.RequestDispatcher; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = RestNotesSlate.class) +@WebAppConfiguration +public class ApiDocumentation { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + @Autowired + private NoteRepository noteRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).snippets() + .withTemplateFormat(markdown())).build(); + } + + @Test + public void errorExample() throws Exception { + this.mockMvc + .perform(get("/error") + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, + "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, + "The tag 'http://localhost:8080/tags/123' does not exist")) + .andDo(print()).andExpect(status().isBadRequest()) + .andExpect(jsonPath("error", is("Bad Request"))) + .andExpect(jsonPath("timestamp", is(notNullValue()))) + .andExpect(jsonPath("status", is(400))) + .andExpect(jsonPath("path", is(notNullValue()))) + .andDo(document("error-example", + responseFields( + 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", + links( + linkWithRel("notes").description("The [Notes](#notes) resource"), + linkWithRel("tags").description("The [Tags](#tags) resource"), + linkWithRel("profile").description("The ALPS profile for the service")), + responseFields( + fieldWithPath("_links").description("Links to other resources")))); + + } + + @Test + public void notesListExample() throws Exception { + this.noteRepository.deleteAll(); + + createNote("REST maturity model", + "http://martinfowler.com/articles/richardsonMaturityModel.html"); + createNote("Hypertext Application Language (HAL)", + "http://stateless.co/hal_specification.html"); + createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); + + this.mockMvc.perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(document("notes-list-example", + responseFields( + fieldWithPath("_embedded.notes").description("An array of [Note](#note) resources")))); + } + + @Test + public void notesCreateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + note.put("tags", Arrays.asList(tagLocation)); + + this.mockMvc.perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))).andExpect( + status().isCreated()) + .andDo(document("notes-create-example", + requestFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("tags").description("An array of [Tag](#tag) resource URIs")))); + } + + @Test + public void noteGetExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + note.put("tags", Arrays.asList(tagLocation)); + + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(document("note-get-example", + links( + linkWithRel("self").description("This note"), + linkWithRel("tags").description("This note's tags")), + responseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("Links to other resources")))); + } + + @Test + public void tagsListExample() throws Exception { + this.noteRepository.deleteAll(); + this.tagRepository.deleteAll(); + + createTag("REST"); + createTag("Hypermedia"); + createTag("HTTP"); + + this.mockMvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(document("tags-list-example", + responseFields( + fieldWithPath("_embedded.tags").description("An array of [Tag](#tag) resources")))); + } + + @Test + public void tagsCreateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + this.mockMvc.perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andDo(document("tags-create-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); + } + + @Test + public void noteUpdateExample() throws Exception { + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))); + + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map noteUpdate = new HashMap(); + noteUpdate.put("tags", Arrays.asList(tagLocation)); + + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("note-update-example", + requestFields( + fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(), + fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(), + fieldWithPath("tags").description("An array of [tag](#tag) resource URIs").optional()))); + } + + @Test + public void tagGetExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(document("tag-get-example", + links( + linkWithRel("self").description("This tag"), + linkWithRel("notes").description("The notes that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("Links to other resources")))); + } + + @Test + public void tagUpdateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map tagUpdate = new HashMap(); + tagUpdate.put("name", "RESTful"); + + this.mockMvc.perform( + patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("tag-update-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); + } + + private void createNote(String title, String body) { + Note note = new Note(); + note.setTitle(title); + note.setBody(body); + + this.noteRepository.save(note); + } + + private void createTag(String name) { + Tag tag = new Tag(); + tag.setName(name); + this.tagRepository.save(tag); + } +} From 4c5095589ac96a9e41064b1b83b27dedfd3bedc1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:57:51 +0000 Subject: [PATCH 0230/1059] Ensure that REST Assured module is installed before sample is built --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 67a98120c..1c00729c5 100644 --- a/build.gradle +++ b/build.gradle @@ -154,6 +154,7 @@ configure(subprojects - project(":docs")) { subproject -> samples { dependOn 'spring-restdocs-core:install' dependOn 'spring-restdocs-mockmvc:install' + dependOn 'spring-restdocs-restassured:install' restNotesSpringHateoas { workingDir "$projectDir/samples/rest-notes-spring-hateoas" From d84fca4d63b55bf7caf0cf5224875ff984bc927a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Feb 2016 10:47:07 +0000 Subject: [PATCH 0231/1059] Ensure that version updates are applied to Slate sample --- build.gradle | 5 +++ .../build/SampleBuildConfigurer.groovy | 42 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 1c00729c5..f6f40e613 100644 --- a/build.gradle +++ b/build.gradle @@ -171,6 +171,11 @@ samples { restAssured { workingDir "$projectDir/samples/rest-assured" } + + slate { + workingDir "$projectDir/samples/rest-notes-slate" + build false + } } task api (type: Javadoc) { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index c960ee1ea..7ce726de8 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -29,7 +29,7 @@ public class SampleBuildConfigurer { private String workingDir - private boolean verifyIncludes = true + private boolean build = true SampleBuildConfigurer(String name) { this.name = name @@ -39,31 +39,41 @@ public class SampleBuildConfigurer { this.workingDir = workingDir } + void build(boolean build) { + this.build = build + } + Task createTask(Project project, Object... dependencies) { File sampleDir = new File(this.workingDir).absoluteFile + Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" + if (new File(sampleDir, 'build.gradle').isFile()) { - Task gradleBuild = createGradleBuild(project, dependencies) - if (verifyIncludes) { + if (build) { + Task gradleBuild = createGradleBuild(project, dependencies) Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) verifyIncludesTask.dependsOn gradleBuild sampleBuild.dependsOn verifyIncludesTask } - else { - sampleBuild.dependsOn gradleBuild + sampleBuild.doFirst { + replaceVersion(new File(this.workingDir, 'build.gradle'), + "springRestdocsVersion = '.*'", + "springRestdocsVersion = '${project.version}'") } } else if (new File(sampleDir, 'pom.xml').isFile()) { - Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - if (verifyIncludes) { + if (build) { + Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) verifyIncludesTask.dependsOn(mavenBuild) sampleBuild.dependsOn verifyIncludesTask } - else { - sampleBuild.dependsOn mavenBuild + sampleBuild.doFirst { + replaceVersion(new File(this.workingDir, 'pom.xml'), + '.*', + "${project.version}") } } else { @@ -79,13 +89,6 @@ public class SampleBuildConfigurer { mavenBuild.workingDir = this.workingDir mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies - - mavenBuild.doFirst { - replaceVersion(new File(this.workingDir, 'pom.xml'), - '.*', - "${project.version}") - } - return mavenBuild } @@ -100,13 +103,6 @@ public class SampleBuildConfigurer { gradleBuild.dir = this.workingDir gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies - - gradleBuild.doFirst { - replaceVersion(new File(this.workingDir, 'build.gradle'), - "springRestdocsVersion = '.*'", - "springRestdocsVersion = '${project.version}'") - } - return gradleBuild } From 36bacb470bc764b3bd58314147b120ebb851ee9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Feb 2016 11:11:53 +0000 Subject: [PATCH 0232/1059] Move to the offical SonarQube plugin --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 743c45bb5..b6e05c519 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,13 @@ buildscript { repositories { jcenter() maven { url 'https://repo.spring.io/plugins-release' } + maven { url 'https://plugins.gradle.org/m2/' } } dependencies { classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:1.2' } } @@ -18,10 +20,10 @@ allprojects { } apply plugin: 'samples' -apply plugin: 'sonar-runner' +apply plugin: 'org.sonarqube' -sonarRunner { - sonarProperties { +sonarqube { + properties { property 'sonar.branch', '1.0.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' From ee865ce4ede415b980bcd90626136fa7f370bdb9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 22 Feb 2016 10:13:35 +0000 Subject: [PATCH 0233/1059] Tolerate relocated test-related classes in Spring Boot 1.4 --- spring-restdocs-restassured/build.gradle | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index e7c33f79c..5f495f507 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,3 +1,8 @@ +boolean isPlatformUsingBootOneFour() { + def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] + bootVersion.startsWith('1.4') +} + dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' @@ -13,4 +18,12 @@ dependencies { test { jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" -} \ No newline at end of file +} + +afterEvaluate { + if (project.hasProperty('platformVersion') && platformUsingBootOneFour) { + dependencies { + springIoTestRuntime 'org.springframework.boot:spring-boot-test' + } + } +} From 536e49287d56258c3e144169e26abbfe11c7904d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 1 Mar 2016 09:38:43 +0000 Subject: [PATCH 0234/1059] Update tests to cope with different Content-Type charset attribute --- ...kMvcRestDocumentationIntegrationTests.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 921e3f8b0..162db0d1d 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -159,7 +159,6 @@ public void curlSnippetWithContent() throws Exception { public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andDo(document("curl-snippet-with-query-string")); @@ -341,14 +340,12 @@ public void preprocessedResponse() throws Exception { + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpResponse(asciidoctor(), HttpStatus.OK) - .header("a", "alpha") - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - original.getBytes().length) - .content(original)))); + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK).header("a", "alpha") + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, + original.getBytes().length) + .content(original)))); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( @@ -356,7 +353,7 @@ public void preprocessedResponse() throws Exception { "build/generated-snippets/preprocessed-response/http-response.adoc"), is(snippet(asciidoctor()) .withContents(httpResponse(asciidoctor(), HttpStatus.OK) - .header("Content-Type", "application/json") + .header("Content-Type", "application/json;charset=UTF-8") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) .content(prettyPrinted)))); @@ -428,7 +425,7 @@ public TestController testController() { @RestController private static class TestController { - @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/", produces = "application/json;charset=UTF-8") public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); From b26d8c085ddcab7af6c62991839e787cc4d4f577 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 1 Mar 2016 16:35:32 -0500 Subject: [PATCH 0235/1059] Add support for generating an HTTPie snippet This commit adds support for generating a snippet that contains the HTTPie command for the request. As the snippet does not require any additional configuration, it has added to the existing default snippets. Httpie does not currently support setting the content type for each part in a multipart form -- these multipart types are currently ignored. See: https://github.com/jkbrzt/httpie/issues/199 https://github.com/jkbrzt/httpie/issues/271 https://github.com/jkbrzt/httpie/pull/285 https://github.com/jkbrzt/httpie/pull/398 There is an issue with specifying piped input for multipart form data. There is no way currently to specify the data without specifying a filename parameter in the Content-Disposition. For now, this is ignored. See: https://github.com/jkbrzt/httpie/issues/342 See gh-207 --- config/checkstyle/checkstyle.xml | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 4 + docs/src/docs/asciidoc/getting-started.adoc | 3 +- .../mockmvc/CustomDefaultSnippets.java | 3 +- .../restassured/CustomDefaultSnippets.java | 3 +- .../restdocs/cli/AbstractCliSnippet.java | 182 ++++++++++ .../{curl => cli}/QueryStringParser.java | 2 +- .../restdocs/cli/curl/CurlDocumentation.java | 60 ++++ .../restdocs/cli/curl/CurlRequestSnippet.java | 161 +++++++++ .../restdocs/{ => cli}/curl/package-info.java | 2 +- .../cli/httpie/HttpieDocumentation.java | 59 ++++ .../cli/httpie/HttpieRequestSnippet.java | 183 ++++++++++ .../restdocs/cli/httpie/package-info.java | 20 ++ .../restdocs/config/SnippetConfigurer.java | 11 +- .../restdocs/curl/CurlDocumentation.java | 9 +- .../restdocs/curl/CurlRequestSnippet.java | 230 +----------- .../default-httpie-request.snippet | 4 + .../markdown/default-httpie-request.snippet | 3 + .../{curl => cli}/QueryStringParserTests.java | 2 +- .../curl/CurlRequestSnippetTests.java | 2 +- .../cli/httpie/HttpieRequestSnippetTests.java | 330 ++++++++++++++++++ .../RestDocumentationConfigurerTests.java | 6 +- .../restdocs/test/ExpectedSnippet.java | 5 + .../httpie-request-with-title.snippet | 5 + .../httpie-request-with-title.snippet | 4 + ...kMvcRestDocumentationIntegrationTests.java | 2 +- 26 files changed, 1061 insertions(+), 236 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{curl => cli}/QueryStringParser.java (98%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{ => cli}/curl/package-info.java (93%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{curl => cli}/QueryStringParserTests.java (98%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{ => cli}/curl/CurlRequestSnippetTests.java (99%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 36c8db5b7..86947f995 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index fb9977c85..f036275c9 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -542,6 +542,10 @@ A number of snippets are produced automatically when you document a request and | Contains the http://curl.haxx.se[`curl`] command that is equivalent to the `MockMvc` call that is being documented +| `httpie-request.adoc` +| Contains the http://httpie.org[`HTTPie`] command that is equivalent to the `MockMvc` +call that is being documented + | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being documented diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 67d7dc178..2e2cc2e3f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -371,9 +371,10 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, three snippets are written: +By default, four snippets are written: * `/index/curl-request.adoc` + * `/index/httpie-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 4be61c375..7164e0581 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -20,12 +20,11 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index ae581cd05..632507c6a 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -19,12 +19,11 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java new file mode 100644 index 000000000..ef00f0ba6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java @@ -0,0 +1,182 @@ +/* + * Copyright 2014-2016 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.cli; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Base64Utils; + +/** + * An abstract {@link Snippet} that for CLI requests. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class AbstractCliSnippet extends TemplatedSnippet { + + private static final Set HEADER_FILTERS; + + static { + Set headerFilters = new HashSet<>(); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); + headerFilters.add(new BasicAuthHeaderFilter()); + HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); + } + + /** + * Create a new abstract cli snippet with the given name and attributes. + * @param snippetName The snippet name. + * @param attributes The snippet attributes. + */ + protected AbstractCliSnippet(String snippetName, Map attributes) { + super(snippetName, attributes); + } + + /** + * Create the model which will be passed to the template for rendering. + * @param operation The operation + * @return The model. + */ + protected abstract Map createModel(Operation operation); + + /** + * Gets the unique parameters given a request. + * @param request The operation request. + * @return The unique parameters. + */ + protected Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Map.Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Map.Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } + } + } + } + + /** + * Whether the request operation is a PUT or a POST. + * @param request The request. + * @return boolean + */ + protected boolean isPutOrPost(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) + || HttpMethod.POST.equals(request.getMethod()); + } + + /** + * Whether the passed header is allowed according to the configured + * header filters. + * @param header The header to test. + * @return boolean + */ + protected boolean allowedHeader(Map.Entry> header) { + for (HeaderFilter headerFilter : HEADER_FILTERS) { + if (!headerFilter.allow(header.getKey(), header.getValue())) { + return false; + } + } + return true; + } + + /** + * Determine if the header passed is a basic auth header. + * @param headerValue The header to test. + * @return boolean + */ + protected boolean isBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.isBasicAuthHeader(headerValue); + } + + /** + * Decodes a basic auth header into name:password credentials. + * @param headerValue The encoded header value. + * @return name:password credentials. + */ + protected String decodeBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + } + + private interface HeaderFilter { + + boolean allow(String name, List value); + } + + private static final class BasicAuthHeaderFilter implements HeaderFilter { + + @Override + public boolean allow(String name, List value) { + return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)); + } + + static boolean isBasicAuthHeader(List value) { + return value != null && (!value.isEmpty()) + && value.get(0).startsWith("Basic "); + } + + static String decodeBasicAuthHeader(List value) { + return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + } + + } + + private static final class NamedHeaderFilter implements HeaderFilter { + + private final String name; + + NamedHeaderFilter(String name) { + this.name = name; + } + + @Override + public boolean allow(String name, List value) { + return !this.name.equalsIgnoreCase(name); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java similarity index 98% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java index ea5f31481..4a3ab8ca1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.io.UnsupportedEncodingException; import java.net.URI; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java new file mode 100644 index 000000000..73ef5fe4d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java @@ -0,0 +1,60 @@ +/* + * 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.cli.curl; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the cURL command-line utility. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + */ +public abstract class CurlDocumentation { + + private CurlDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. + * + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest() { + return new CurlRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest(Map attributes) { + return new CurlRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java new file mode 100644 index 000000000..af4c253c8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014-2016 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.cli.curl; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the curl command for a request. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @see CurlDocumentation#curlRequest() + * @see CurlDocumentation#curlRequest(Map) + */ +public class CurlRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected CurlRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected CurlRequestSnippet(Map attributes) { + super("curl-request", attributes); + } + + @Override + protected Map createModel(Operation operation) { + Map model = new HashMap<>(); + model.put("url", getUrl(operation)); + model.put("options", getOptions(operation)); + return model; + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getOptions(Operation operation) { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + writeIncludeHeadersInOutputOption(printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeHttpMethodIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest().getHeaders(), printer); + writePartsIfNecessary(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + + return command.toString(); + } + + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { + writer.print("-i"); + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format(" -u '%s'", credentials)); + } + } + + private void writeHttpMethodIfNecessary(OperationRequest request, + PrintWriter writer) { + if (!HttpMethod.GET.equals(request.getMethod())) { + writer.print(String.format(" -X %s", request.getMethod())); + } + } + + private void writeHeaders(HttpHeaders headers, PrintWriter writer) { + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } + } + } + } + + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" -F '%s=", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + writer.append(part.getContentAsString()); + } + else { + writer.printf("@%s", part.getSubmittedFileName()); + } + if (part.getHeaders().getContentType() != null) { + writer.append(";type=") + .append(part.getHeaders().getContentType().toString()); + } + + writer.append("'"); + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + writer.print(String.format(" -d '%s'", content)); + } + else if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java similarity index 93% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java index 7c07b91e0..02ebe62cd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java @@ -17,4 +17,4 @@ /** * Documenting the curl command required to make a request to a RESTful API. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java new file mode 100644 index 000000000..0313ae027 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java @@ -0,0 +1,59 @@ +/* + * 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.cli.httpie; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the httpie command-line utility. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class HttpieDocumentation { + + private HttpieDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. + * + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest() { + return new HttpieRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest(Map attributes) { + return new HttpieRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java new file mode 100644 index 000000000..7e251a90e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014-2016 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.cli.httpie; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the httpie command for a request. + * + * @author Raman Gupta + * @see HttpieDocumentation#httpieRequest() + * @see HttpieDocumentation#httpieRequest(Map) + */ +public class HttpieRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected HttpieRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpieRequestSnippet(Map attributes) { + super("httpie-request", attributes); + } + + @Override + protected Map createModel(final Operation operation) { + Map model = new HashMap<>(); + model.put("echo_content", getContentStdIn(operation)); + model.put("options", getOptions(operation)); + model.put("url", getUrl(operation)); + model.put("request_items", getRequestItems(operation)); + return model; + } + private Object getContentStdIn(final Operation operation) { + OperationRequest request = operation.getRequest(); + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + return String.format("echo '%s' | ", content); + } + else { + return ""; + } + } + + private String getOptions(Operation operation) { + StringWriter options = new StringWriter(); + PrintWriter printer = new PrintWriter(options); + writeOptions(operation.getRequest(), printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeMethodIfNecessary(operation.getRequest(), printer); + return options.toString(); + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getRequestItems(final Operation operation) { + StringWriter requestItems = new StringWriter(); + PrintWriter printer = new PrintWriter(requestItems); + writeFormDataIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + return requestItems.toString(); + } + private void writeOptions(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + if (request.getParts().size() > 0 || uniqueParameters.size() > 0) { + writer.print("--form "); + } + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format("--auth '%s' ", credentials)); + } + } + + private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { + writer.print(String.format("%s", request.getMethod().name())); + } + + private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" \\\n '%s'", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + // httpie https://github.com/jkbrzt/httpie/issues/342 + writer.printf("@<(echo '%s')", part.getContentAsString()); + } + else { + writer.printf("@'%s'", part.getSubmittedFileName()); + } + // httpie does not currently support manually set content type by part + } + } + + private void writeHeaders(OperationRequest request, PrintWriter writer) { + HttpHeaders headers = request.getHeaders(); + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + // form Content-Type not required, added automatically by httpie with --form + if (request.getParts().size() > 0 + && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && header.startsWith("multipart/form-data")) { + continue; + } + writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + } + } + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (!StringUtils.hasText(content)) { + if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + for (Map.Entry> entry : uniqueParameters.entrySet()) { + if (entry.getValue().isEmpty()) { + writer.append(String.format(" '%s='", entry.getKey())); + } + else { + for (String value : entry.getValue()) { + writer.append(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java new file mode 100644 index 000000000..2bdf5df66 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the httpie command required to make a request to a RESTful API. + */ +package org.springframework.restdocs.cli.httpie; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index e8faa6ecf..7c2b459c7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -21,7 +21,8 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.httpie.HttpieDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -38,8 +39,12 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList(CurlDocumentation.curlRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); + private List defaultSnippets = Arrays.asList( + CurlDocumentation.curlRequest(), + HttpieDocumentation.httpieRequest(), + HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse() + ); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a3549443b..a9b094f6c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -24,11 +24,14 @@ * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda * @author Jonathan Pearlin */ +@Deprecated public abstract class CurlDocumentation { private CurlDocumentation() { @@ -40,6 +43,8 @@ private CurlDocumentation() { * operation. * * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest() { return new CurlRequestSnippet(); @@ -52,6 +57,8 @@ public static Snippet curlRequest() { * * @param attributes the attributes * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4f4ea07b7..29396bac6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -16,243 +16,35 @@ package org.springframework.restdocs.curl; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; /** * A {@link Snippet} that documents the curl command for a request. * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @see CurlDocumentation#curlRequest() - * @see CurlDocumentation#curlRequest(Map) + * @author Raman Gupta + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ -public class CurlRequestSnippet extends TemplatedSnippet { - - private static final Set HEADER_FILTERS; - - static { - Set headerFilters = new HashSet<>(); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); - headerFilters.add(new BasicAuthHeaderFilter()); - HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); - } +public class CurlRequestSnippet extends org.springframework.restdocs.cli.curl.CurlRequestSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ protected CurlRequestSnippet() { - this(null); + super(); } /** - * Creates a new {@code CurlRequestSnippet} with the given additional - * {@code attributes} that will be included in the model during template rendering. + * Creates a new {@code CurlRequestSnippet} with additional attributes. + * @param attributes The additional attributes. * - * @param attributes The additional attributes + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ - protected CurlRequestSnippet(Map attributes) { - super("curl-request", attributes); - } - - @Override - protected Map createModel(Operation operation) { - Map model = new HashMap<>(); - model.put("url", getUrl(operation)); - model.put("options", getOptions(operation)); - return model; - } - - private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); - } - - private String getOptions(Operation operation) { - StringWriter command = new StringWriter(); - PrintWriter printer = new PrintWriter(command); - writeIncludeHeadersInOutputOption(printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest().getHeaders(), printer); - writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); - - return command.toString(); - } - - private void writeIncludeHeadersInOutputOption(PrintWriter writer) { - writer.print("-i"); - } - - private void writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { - String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); - writer.print(String.format(" -u '%s'", credentials)); - } - } - - private void writeHttpMethodIfNecessary(OperationRequest request, - PrintWriter writer) { - if (!HttpMethod.GET.equals(request.getMethod())) { - writer.print(String.format(" -X %s", request.getMethod())); - } - } - - private void writeHeaders(HttpHeaders headers, PrintWriter writer) { - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } - } - } - } - - private boolean allowedHeader(Entry> header) { - for (HeaderFilter headerFilter : HEADER_FILTERS) { - if (!headerFilter.allow(header.getKey(), header.getValue())) { - return false; - } - } - return true; - } - - private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" -F '%s=", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - writer.append(part.getContentAsString()); - } - else { - writer.printf("@%s", part.getSubmittedFileName()); - } - if (part.getHeaders().getContentType() != null) { - writer.append(";type=") - .append(part.getHeaders().getContentType().toString()); - } - - writer.append("'"); - } - } - - private void writeContent(OperationRequest request, PrintWriter writer) { - String content = request.getContentAsString(); - if (StringUtils.hasText(content)) { - writer.print(String.format(" -d '%s'", content)); - } - else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (isPutOrPost(request)) { - writeContentUsingParameters(request, writer); - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - String queryString = uniqueParameters.toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); - } - } - - private Parameters getUniqueParameters(OperationRequest request) { - Parameters queryStringParameters = new QueryStringParser() - .parse(request.getUri()); - Parameters uniqueParameters = new Parameters(); - - for (Entry> parameter : request.getParameters().entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - - private boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); - } - - private interface HeaderFilter { - - boolean allow(String name, List value); - } - - private static final class BasicAuthHeaderFilter implements HeaderFilter { - - @Override - public boolean allow(String name, List value) { - if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { - return false; - } - return true; - } - - static boolean isBasicAuthHeader(List value) { - return value != null && (!value.isEmpty()) - && value.get(0).startsWith("Basic "); - } - - static String decodeBasicAuthHeader(List value) { - return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); - } - - } - - private static final class NamedHeaderFilter implements HeaderFilter { - - private final String name; - - private NamedHeaderFilter(String name) { - this.name = name; - } - - @Override - public boolean allow(String name, List value) { - return !this.name.equalsIgnoreCase(name); - } - + protected CurlRequestSnippet(final Map attributes) { + super(attributes); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet new file mode 100644 index 000000000..05771238f --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet @@ -0,0 +1,4 @@ +[source,bash] +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet new file mode 100644 index 000000000..acddb53f0 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet @@ -0,0 +1,3 @@ +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java similarity index 98% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java index 634fede11..6a953e16a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.net.URI; import java.util.Arrays; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java index 2b1371924..0d8bb6324 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; import java.io.IOException; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java new file mode 100644 index 000000000..1fb2abd7a --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2014-2016 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.cli.httpie; + +import java.io.IOException; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.util.Base64Utils; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link HttpieRequestSnippet}. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +@RunWith(Parameterized.class) +public class HttpieRequestSnippetTests extends AbstractSnippetTests { + + public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void getRequest() throws IOException { + this.snippet.expectHttpieRequest("get-request").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document( + operationBuilder("get-request").request("http://localhost/foo").build()); + } + + @Test + public void nonGetRequest() throws IOException { + this.snippet.expectHttpieRequest("non-get-request").withContents( + codeBlock("bash").content("$ http POST 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); + } + + @Test + public void requestWithContent() throws IOException { + this.snippet.expectHttpieRequest("request-with-content") + .withContents(codeBlock("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); + } + + @Test + public void getRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-query-string") + .request("http://localhost/foo?param=value").build()); + } + + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("request-with-query-string-with-no-value") + .request("http://localhost/foo?param").build()); + } + + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string") + .request("http://localhost/foo?param=value").method("POST") + .build()); + } + + @Test + public void postRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-with-no-value") + .request("http://localhost/foo?param").method("POST").build()); + } + + @Test + public void postRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-one-parameter") + .request("http://localhost/foo").method("POST").param("k1", "v1") + .build()); + } + + @Test + public void postRequestWithOneParameterWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter-with-no-value") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); + new HttpieRequestSnippet().document( + operationBuilder("post-request-with-one-parameter-with-no-value") + .request("http://localhost/foo").method("POST").param("k1") + .build()); + } + + @Test + public void postRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-multiple-parameters") + .request("http://localhost/foo").method("POST") + .param("k1", "v1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void postRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); + } + + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-and-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-and-parameter") + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "post-request-with-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void putRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-one-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .build()); + } + + @Test + public void putRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-multiple-parameters") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void putRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "a&b") + .build()); + } + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectHttpieRequest("request-with-headers") + .withContents(codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Content-Type:application/json' 'a:alpha'")); + new HttpieRequestSnippet().document( + operationBuilder("request-with-headers").request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + + @Test + public void multipartPostWithNoSubmittedFileName() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'metadata'@<(echo '{\"description\": \"foo\"}')"; + this.snippet.expectHttpieRequest("multipart-post-no-original-filename") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-no-original-filename") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); + } + + @Test + public void multipartPostWithContentType() throws IOException { + // httpie does not yet support manually set content type by part + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post-with-content-type") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-content-type") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPost() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPostWithParameters() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado' 'b=banana'"; + this.snippet.expectHttpieRequest("multipart-post-with-parameters") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-parameters") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); + } + + @Test + public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") + .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); + new HttpieRequestSnippet() + .document(operationBuilder("basic-auth").request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils + .encodeToString("user:secret".getBytes())) + .build()); + } + + @Test + public void customAttributes() throws IOException { + this.snippet.expectHttpieRequest("custom-attributes") + .withContents(containsString("httpie request title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("httpie-request")) + .willReturn(snippetResource("httpie-request-with-title")); + new HttpieRequestSnippet( + attributes( + key("title").value("httpie request title"))) + .document( + operationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost/foo").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0462fb306..7a36e3200 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,8 +24,9 @@ import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; -import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.httpie.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; @@ -71,6 +72,7 @@ public void defaultConfiguration() { .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), instanceOf(HttpResponseSnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index d706ddf6e..2799f1cc0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -76,6 +76,11 @@ public ExpectedSnippet expectCurlRequest(String name) { return this; } + public ExpectedSnippet expectHttpieRequest(String name) { + expect(name, "httpie-request"); + return this; + } + public ExpectedSnippet expectRequestFields(String name) { expect(name, "request-fields"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet new file mode 100644 index 000000000..0bff58b19 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet @@ -0,0 +1,5 @@ +[source,bash] +.{{title}} +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet new file mode 100644 index 000000000..29beade11 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet @@ -0,0 +1,4 @@ +{{title}} +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 162db0d1d..ad06b187b 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,7 +56,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; From 37e16bc308b08d465b0a3aee3adbeeaa70877b86 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Mar 2016 17:23:04 +0000 Subject: [PATCH 0236/1059] Polish contribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit polishes the HTTPie request snippet contribution made in b26d8c0. It makes the following significant changes: - Applies project’s coding conventions for formatting and the like - Moves to a composition-based approach for sharing functionality between the curl and HTTPie snippets by replacing AbstractCliSnippet with CliOperationRequest. - Introduces a single package for CLI command snippets, thereby allowing more code to be package-private. See gh-207 --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 3 +- docs/src/docs/asciidoc/getting-started.adoc | 2 +- .../mockmvc/CustomDefaultSnippets.java | 10 +- .../restassured/CustomDefaultSnippets.java | 15 +- ...cumentation.java => CliDocumentation.java} | 39 +++- ...iSnippet.java => CliOperationRequest.java} | 128 ++++++------ .../cli/{curl => }/CurlRequestSnippet.java | 48 +++-- .../restdocs/cli/HttpieRequestSnippet.java | 175 +++++++++++++++++ .../cli/httpie/HttpieDocumentation.java | 59 ------ .../cli/httpie/HttpieRequestSnippet.java | 183 ------------------ .../cli/{httpie => }/package-info.java | 6 +- .../restdocs/config/SnippetConfigurer.java | 12 +- .../restdocs/curl/CurlDocumentation.java | 12 +- .../restdocs/curl/CurlRequestSnippet.java | 17 +- .../restdocs/{cli => }/curl/package-info.java | 5 +- .../default-httpie-request.snippet | 2 +- .../markdown/default-httpie-request.snippet | 2 +- .../{curl => }/CurlRequestSnippetTests.java | 2 +- .../HttpieRequestSnippetTests.java | 16 +- .../RestDocumentationConfigurerTests.java | 8 +- .../httpie-request-with-title.snippet | 2 +- .../httpie-request-with-title.snippet | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 34 +++- 24 files changed, 396 insertions(+), 388 deletions(-) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{curl/CurlDocumentation.java => CliDocumentation.java} (59%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{AbstractCliSnippet.java => CliOperationRequest.java} (59%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{curl => }/CurlRequestSnippet.java (74%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{httpie => }/package-info.java (75%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{cli => }/curl/package-info.java (83%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/{curl => }/CurlRequestSnippetTests.java (99%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/{httpie => }/HttpieRequestSnippetTests.java (96%) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 86947f995..061bb5018 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.CliDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index a290c72ce..00c31ac61 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -87,11 +87,12 @@ include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-fo [[configuration-default-snippets]] === Default snippets -Three snippets are produced by default: +Four snippets are produced by default: - `curl-request` - `http-request` - `http-response` +- `httpie-request` You can change the default snippet configuration during setup using the `RestDocumentationConfigurer` API. For example, to only produce the `curl-request` diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2e2cc2e3f..daed3a4f1 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -374,9 +374,9 @@ static `document` method on By default, four snippets are written: * `/index/curl-request.adoc` - * `/index/httpie-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` + * `/index/httpie-request.adoc` Refer to <> for more information about these and other snippets that can be produced by Spring REST Docs. diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 7164e0581..4fb2990f8 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -18,19 +18,21 @@ import org.junit.Before; import org.junit.Rule; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( + "build"); @Autowired private WebApplicationContext context; @@ -41,8 +43,8 @@ public class CustomDefaultSnippets { public void setUp() { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation) - .snippets().withDefaults(curlRequest())) + .apply(documentationConfiguration(this.restDocumentation).snippets() + .withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 632507c6a..57c65d360 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -16,20 +16,21 @@ package com.example.restassured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; import org.junit.Before; import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.JUnitRestDocumentation; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( + "build"); private RequestSpecification spec; @@ -37,8 +38,8 @@ public class CustomDefaultSnippets { public void setUp() { // tag::custom-default-snippets[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withDefaults(curlRequest())) + .addFilter(documentationConfiguration(this.restDocumentation).snippets() + .withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java similarity index 59% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java index 73ef5fe4d..9b6b7675f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -14,24 +14,23 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.util.Map; import org.springframework.restdocs.snippet.Snippet; /** - * Static factory methods for documenting a RESTful API as if it were being driven using - * the cURL command-line utility. + * Static factory methods for documenting a RESTful API as if it were being driven using a + * command-line utility such as curl or HTTPie. * * @author Andy Wilkinson - * @author Yann Le Guern - * @author Dmitriy Mayboroda - * @author Jonathan Pearlin + * @author Paul-Christian Volkmer + * @author Raman Gupta */ -public abstract class CurlDocumentation { +public abstract class CliDocumentation { - private CurlDocumentation() { + private CliDocumentation() { } @@ -57,4 +56,26 @@ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); } + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. + * + * @return the snippet that will document the HTTPie request + */ + public static Snippet httpieRequest() { + return new HttpieRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the HTTPie request + */ + public static Snippet httpieRequest(Map attributes) { + return new HttpieRequestSnippet(attributes); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java similarity index 59% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index ef00f0ba6..99ce6aeba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2012-2016 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. @@ -16,29 +16,30 @@ package org.springframework.restdocs.cli; +import java.net.URI; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; /** - * An abstract {@link Snippet} that for CLI requests. + * An {@link OperationRequest} wrapper with methods that are useful when producing a + * snippet containing a CLI command for a request. * * @author Andy Wilkinson - * @author Paul-Christian Volkmer * @author Raman Gupta */ -public abstract class AbstractCliSnippet extends TemplatedSnippet { +final class CliOperationRequest implements OperationRequest { private static final Set HEADER_FILTERS; @@ -50,33 +51,19 @@ public abstract class AbstractCliSnippet extends TemplatedSnippet { HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); } - /** - * Create a new abstract cli snippet with the given name and attributes. - * @param snippetName The snippet name. - * @param attributes The snippet attributes. - */ - protected AbstractCliSnippet(String snippetName, Map attributes) { - super(snippetName, attributes); - } - - /** - * Create the model which will be passed to the template for rendering. - * @param operation The operation - * @return The model. - */ - protected abstract Map createModel(Operation operation); - - /** - * Gets the unique parameters given a request. - * @param request The operation request. - * @return The unique parameters. - */ - protected Parameters getUniqueParameters(OperationRequest request) { + private final OperationRequest delegate; + + CliOperationRequest(OperationRequest delegate) { + this.delegate = delegate; + } + + Parameters getUniqueParameters() { Parameters queryStringParameters = new QueryStringParser() - .parse(request.getUri()); + .parse(this.delegate.getUri()); Parameters uniqueParameters = new Parameters(); - for (Map.Entry> parameter : request.getParameters().entrySet()) { + for (Map.Entry> parameter : this.delegate.getParameters() + .entrySet()) { addIfUnique(parameter, queryStringParameters, uniqueParameters); } return uniqueParameters; @@ -98,23 +85,42 @@ private void addIfUnique(Map.Entry> parameter, } } - /** - * Whether the request operation is a PUT or a POST. - * @param request The request. - * @return boolean - */ - protected boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); + boolean isPutOrPost() { + return HttpMethod.PUT.equals(this.delegate.getMethod()) + || HttpMethod.POST.equals(this.delegate.getMethod()); + } + + String getBasicAuthCredentials() { + List headerValue = this.delegate.getHeaders() + .get(HttpHeaders.AUTHORIZATION); + if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { + return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + } + return null; + } + + @Override + public byte[] getContent() { + return this.delegate.getContent(); + } + + @Override + public String getContentAsString() { + return this.delegate.getContentAsString(); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders filteredHeaders = new HttpHeaders(); + for (Entry> header : this.delegate.getHeaders().entrySet()) { + if (allowedHeader(header)) { + filteredHeaders.put(header.getKey(), header.getValue()); + } + } + return HttpHeaders.readOnlyHttpHeaders(filteredHeaders); } - /** - * Whether the passed header is allowed according to the configured - * header filters. - * @param header The header to test. - * @return boolean - */ - protected boolean allowedHeader(Map.Entry> header) { + private boolean allowedHeader(Map.Entry> header) { for (HeaderFilter headerFilter : HEADER_FILTERS) { if (!headerFilter.allow(header.getKey(), header.getValue())) { return false; @@ -123,22 +129,24 @@ protected boolean allowedHeader(Map.Entry> header) { return true; } - /** - * Determine if the header passed is a basic auth header. - * @param headerValue The header to test. - * @return boolean - */ - protected boolean isBasicAuthHeader(List headerValue) { - return BasicAuthHeaderFilter.isBasicAuthHeader(headerValue); + @Override + public HttpMethod getMethod() { + return this.delegate.getMethod(); + } + + @Override + public Parameters getParameters() { + return this.delegate.getParameters(); + } + + @Override + public Collection getParts() { + return this.delegate.getParts(); } - /** - * Decodes a basic auth header into name:password credentials. - * @param headerValue The encoded header value. - * @return name:password credentials. - */ - protected String decodeBasicAuthHeader(List headerValue) { - return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + @Override + public URI getUri() { + return this.delegate.getUri(); } private interface HeaderFilter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java similarity index 74% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index af4c253c8..5112b6dc5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.io.PrintWriter; import java.io.StringWriter; @@ -23,14 +23,13 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.restdocs.cli.AbstractCliSnippet; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; /** @@ -38,10 +37,10 @@ * * @author Andy Wilkinson * @author Paul-Christian Volkmer - * @see CurlDocumentation#curlRequest() - * @see CurlDocumentation#curlRequest(Map) + * @see CliDocumentation#curlRequest() + * @see CliDocumentation#curlRequest(Map) */ -public class CurlRequestSnippet extends AbstractCliSnippet { +public class CurlRequestSnippet extends TemplatedSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. @@ -76,11 +75,12 @@ private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeIncludeHeadersInOutputOption(printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest().getHeaders(), printer); - writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); + CliOperationRequest request = new CliOperationRequest(operation.getRequest()); + writeUserOptionIfNecessary(request, printer); + writeHttpMethodIfNecessary(request, printer); + writeHeaders(request, printer); + writePartsIfNecessary(request, printer); + writeContent(request, printer); return command.toString(); } @@ -89,11 +89,10 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private void writeUserOptionIfNecessary(OperationRequest request, + private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (isBasicAuthHeader(headerValue)) { - String credentials = decodeBasicAuthHeader(headerValue); + String credentials = request.getBasicAuthCredentials(); + if (credentials != null) { writer.print(String.format(" -u '%s'", credentials)); } } @@ -105,12 +104,10 @@ private void writeHttpMethodIfNecessary(OperationRequest request, } } - private void writeHeaders(HttpHeaders headers, PrintWriter writer) { - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } + private void writeHeaders(CliOperationRequest request, PrintWriter writer) { + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } } @@ -133,7 +130,7 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) } } - private void writeContent(OperationRequest request, PrintWriter writer) { + private void writeContent(CliOperationRequest request, PrintWriter writer) { String content = request.getContentAsString(); if (StringUtils.hasText(content)) { writer.print(String.format(" -d '%s'", content)); @@ -145,17 +142,18 @@ else if (!request.getParts().isEmpty()) { } } } - else if (isPutOrPost(request)) { + else if (request.isPutOrPost()) { writeContentUsingParameters(request, writer); } } - private void writeContentUsingParameters(OperationRequest request, + private void writeContentUsingParameters(CliOperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); + Parameters uniqueParameters = request.getUniqueParameters(); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { writer.print(String.format(" -d '%s'", queryString)); } } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java new file mode 100644 index 000000000..0d132e417 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -0,0 +1,175 @@ +/* + * Copyright 2014-2016 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.cli; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the HTTPie command for a request. + * + * @author Raman Gupta + * @author Andy Wilkinson + * @see CliDocumentation#httpieRequest() + * @see CliDocumentation#httpieRequest(Map) + */ +public class HttpieRequestSnippet extends TemplatedSnippet { + + /** + * Creates a new {@code HttpieRequestSnippet} with no additional attributes. + */ + protected HttpieRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code HttpieRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpieRequestSnippet(Map attributes) { + super("httpie-request", attributes); + } + + @Override + protected Map createModel(Operation operation) { + Map model = new HashMap<>(); + CliOperationRequest request = new CliOperationRequest(operation.getRequest()); + model.put("echoContent", getContentStandardIn(request)); + model.put("options", getOptions(request)); + model.put("url", getUrl(request)); + model.put("requestItems", getRequestItems(request)); + return model; + } + + private Object getContentStandardIn(CliOperationRequest request) { + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + return String.format("echo '%s' | ", content); + } + return ""; + } + + private String getOptions(CliOperationRequest request) { + StringWriter options = new StringWriter(); + PrintWriter printer = new PrintWriter(options); + writeOptions(request, printer); + writeUserOptionIfNecessary(request, printer); + writeMethodIfNecessary(request, printer); + return options.toString(); + } + + private String getUrl(OperationRequest request) { + return String.format("'%s'", request.getUri()); + } + + private String getRequestItems(CliOperationRequest request) { + StringWriter requestItems = new StringWriter(); + PrintWriter printer = new PrintWriter(requestItems); + writeFormDataIfNecessary(request, printer); + writeHeaders(request, printer); + writeParametersIfNecessary(request, printer); + return requestItems.toString(); + } + + private void writeOptions(CliOperationRequest request, PrintWriter writer) { + if (!request.getParts().isEmpty() || !request.getUniqueParameters().isEmpty()) { + writer.print("--form "); + } + } + + private void writeUserOptionIfNecessary(CliOperationRequest request, + PrintWriter writer) { + String credentials = request.getBasicAuthCredentials(); + if (credentials != null) { + writer.print(String.format("--auth '%s' ", credentials)); + } + } + + private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { + writer.print(String.format("%s", request.getMethod().name())); + } + + private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" \\%n '%s'", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + // https://github.com/jkbrzt/httpie/issues/342 + writer.printf("@<(echo '%s')", part.getContentAsString()); + } + else { + writer.printf("@'%s'", part.getSubmittedFileName()); + } + } + } + + private void writeHeaders(OperationRequest request, PrintWriter writer) { + HttpHeaders headers = request.getHeaders(); + for (Entry> entry : headers.entrySet()) { + for (String header : entry.getValue()) { + // HTTPie adds Content-Type automatically with --form + if (!request.getParts().isEmpty() + && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && header.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { + continue; + } + writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + } + } + } + + private void writeParametersIfNecessary(CliOperationRequest request, + PrintWriter writer) { + if (StringUtils.hasText(request.getContentAsString())) { + return; + } + if (!request.getParts().isEmpty()) { + writeContentUsingParameters(request.getParameters(), writer); + } + else if (request.isPutOrPost()) { + writeContentUsingParameters(request.getUniqueParameters(), writer); + } + } + + private void writeContentUsingParameters(Parameters parameters, PrintWriter writer) { + for (Map.Entry> entry : parameters.entrySet()) { + if (entry.getValue().isEmpty()) { + writer.append(String.format(" '%s='", entry.getKey())); + } + else { + for (String value : entry.getValue()) { + writer.append(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java deleted file mode 100644 index 0313ae027..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java +++ /dev/null @@ -1,59 +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.cli.httpie; - -import java.util.Map; - -import org.springframework.restdocs.snippet.Snippet; - -/** - * Static factory methods for documenting a RESTful API as if it were being driven using - * the httpie command-line utility. - * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @author Raman Gupta - */ -public abstract class HttpieDocumentation { - - private HttpieDocumentation() { - - } - - /** - * Returns a new {@code Snippet} that will document the httpie request for the API - * operation. - * - * @return the snippet that will document the httpie request - */ - public static Snippet httpieRequest() { - return new HttpieRequestSnippet(); - } - - /** - * Returns a new {@code Snippet} that will document the httpie request for the API - * operation. The given {@code attributes} will be available during snippet - * generation. - * - * @param attributes the attributes - * @return the snippet that will document the httpie request - */ - public static Snippet httpieRequest(Map attributes) { - return new HttpieRequestSnippet(attributes); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java deleted file mode 100644 index 7e251a90e..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2014-2016 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.cli.httpie; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.cli.AbstractCliSnippet; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.util.StringUtils; - -/** - * A {@link Snippet} that documents the httpie command for a request. - * - * @author Raman Gupta - * @see HttpieDocumentation#httpieRequest() - * @see HttpieDocumentation#httpieRequest(Map) - */ -public class HttpieRequestSnippet extends AbstractCliSnippet { - - /** - * Creates a new {@code CurlRequestSnippet} with no additional attributes. - */ - protected HttpieRequestSnippet() { - this(null); - } - - /** - * Creates a new {@code CurlRequestSnippet} with the given additional - * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes - */ - protected HttpieRequestSnippet(Map attributes) { - super("httpie-request", attributes); - } - - @Override - protected Map createModel(final Operation operation) { - Map model = new HashMap<>(); - model.put("echo_content", getContentStdIn(operation)); - model.put("options", getOptions(operation)); - model.put("url", getUrl(operation)); - model.put("request_items", getRequestItems(operation)); - return model; - } - private Object getContentStdIn(final Operation operation) { - OperationRequest request = operation.getRequest(); - String content = request.getContentAsString(); - if (StringUtils.hasText(content)) { - return String.format("echo '%s' | ", content); - } - else { - return ""; - } - } - - private String getOptions(Operation operation) { - StringWriter options = new StringWriter(); - PrintWriter printer = new PrintWriter(options); - writeOptions(operation.getRequest(), printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeMethodIfNecessary(operation.getRequest(), printer); - return options.toString(); - } - - private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); - } - - private String getRequestItems(final Operation operation) { - StringWriter requestItems = new StringWriter(); - PrintWriter printer = new PrintWriter(requestItems); - writeFormDataIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); - return requestItems.toString(); - } - private void writeOptions(OperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - if (request.getParts().size() > 0 || uniqueParameters.size() > 0) { - writer.print("--form "); - } - } - - private void writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (isBasicAuthHeader(headerValue)) { - String credentials = decodeBasicAuthHeader(headerValue); - writer.print(String.format("--auth '%s' ", credentials)); - } - } - - private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { - writer.print(String.format("%s", request.getMethod().name())); - } - - private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" \\\n '%s'", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - // httpie https://github.com/jkbrzt/httpie/issues/342 - writer.printf("@<(echo '%s')", part.getContentAsString()); - } - else { - writer.printf("@'%s'", part.getSubmittedFileName()); - } - // httpie does not currently support manually set content type by part - } - } - - private void writeHeaders(OperationRequest request, PrintWriter writer) { - HttpHeaders headers = request.getHeaders(); - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - // form Content-Type not required, added automatically by httpie with --form - if (request.getParts().size() > 0 - && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) - && header.startsWith("multipart/form-data")) { - continue; - } - writer.print(String.format(" '%s:%s'", entry.getKey(), header)); - } - } - } - } - - private void writeContent(OperationRequest request, PrintWriter writer) { - String content = request.getContentAsString(); - if (!StringUtils.hasText(content)) { - if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" '%s=%s'", entry.getKey(), value)); - } - } - } - else if (isPutOrPost(request)) { - writeContentUsingParameters(request, writer); - } - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - for (Map.Entry> entry : uniqueParameters.entrySet()) { - if (entry.getValue().isEmpty()) { - writer.append(String.format(" '%s='", entry.getKey())); - } - else { - for (String value : entry.getValue()) { - writer.append(String.format(" '%s=%s'", entry.getKey(), value)); - } - } - } - } -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java similarity index 75% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java index 2bdf5df66..f06c96709 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -15,6 +15,6 @@ */ /** - * Documenting the httpie command required to make a request to a RESTful API. + * Documenting CLI commands required to make a request to a RESTful API. */ -package org.springframework.restdocs.cli.httpie; +package org.springframework.restdocs.cli; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 7c2b459c7..cb72f152c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -21,8 +21,7 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.cli.curl.CurlDocumentation; -import org.springframework.restdocs.cli.httpie.HttpieDocumentation; +import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -39,12 +38,9 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList( - CurlDocumentation.curlRequest(), - HttpieDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse() - ); + private List defaultSnippets = Arrays.asList(CliDocumentation.curlRequest(), + CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse()); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a9b094f6c..123c7a663 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -24,8 +24,8 @@ * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. - * + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation}. * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda @@ -44,8 +44,10 @@ private CurlDocumentation() { * * @return the snippet that will document the curl request * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest()}. */ + @Deprecated public static Snippet curlRequest() { return new CurlRequestSnippet(); } @@ -58,8 +60,10 @@ public static Snippet curlRequest() { * @param attributes the attributes * @return the snippet that will document the curl request * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest(Map)}. */ + @Deprecated public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 29396bac6..18148392a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -23,16 +23,23 @@ /** * A {@link Snippet} that documents the curl command for a request. * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer * @author Raman Gupta - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ -public class CurlRequestSnippet extends org.springframework.restdocs.cli.curl.CurlRequestSnippet { +@Deprecated +public class CurlRequestSnippet + extends org.springframework.restdocs.cli.CurlRequestSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ + @Deprecated protected CurlRequestSnippet() { super(); } @@ -41,8 +48,10 @@ protected CurlRequestSnippet() { * Creates a new {@code CurlRequestSnippet} with additional attributes. * @param attributes The additional attributes. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ + @Deprecated protected CurlRequestSnippet(final Map attributes) { super(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java similarity index 83% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java index 02ebe62cd..e614e2b84 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -16,5 +16,8 @@ /** * Documenting the curl command required to make a request to a RESTful API. + * + * @deprecated Since 1.1 in favor of functionality in + * {@code org.springframework.restdocs.cli} */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.curl; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet index 05771238f..1b690a5f5 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet @@ -1,4 +1,4 @@ [source,bash] ---- -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet index acddb53f0..65d78f740 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet @@ -1,3 +1,3 @@ ```bash -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 0d8bb6324..bb7cff803 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.io.IOException; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java similarity index 96% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 1fb2abd7a..6194fef91 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.httpie; +package org.springframework.restdocs.cli; import java.io.IOException; @@ -156,8 +156,8 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(operationBuilder("post-request-with-url-encoded-parameter") .request("http://localhost/foo").method("POST").param("k1", "a&b") @@ -214,8 +214,8 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(operationBuilder("put-request-with-url-encoded-parameter") .request("http://localhost/foo").method("PUT").param("k1", "a&b") @@ -224,8 +224,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest("request-with-headers") - .withContents(codeBlock("bash").content("$ http GET 'http://localhost/foo'" + this.snippet.expectHttpieRequest("request-with-headers").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); new HttpieRequestSnippet().document( operationBuilder("request-with-headers").request("http://localhost/foo") @@ -298,7 +298,7 @@ public void multipartPostWithParameters() throws IOException { } @Test - public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 7a36e3200..7198ffc85 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,9 +24,9 @@ import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.cli.curl.CurlDocumentation; -import org.springframework.restdocs.cli.curl.CurlRequestSnippet; -import org.springframework.restdocs.cli.httpie.HttpieRequestSnippet; +import org.springframework.restdocs.cli.CliDocumentation; +import org.springframework.restdocs.cli.CurlRequestSnippet; +import org.springframework.restdocs.cli.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; @@ -108,7 +108,7 @@ public void customWriterResolver() { public void customDefaultSnippets() { RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.snippets().withDefaults(CurlDocumentation.curlRequest()) + this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()) .apply(configuration, context); assertThat(configuration, hasEntry( diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet index 0bff58b19..7c4eb0483 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet @@ -1,5 +1,5 @@ [source,bash] .{{title}} ---- -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet index 29beade11..67dff4550 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet @@ -1,4 +1,4 @@ {{title}} ```bash -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ``` \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ad06b187b..51c35ee4d 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,7 +56,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -171,6 +171,38 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { + "-H 'Accept: application/json' -d 'a=alpha'")))); } + @Test + public void httpieSnippetWithContent() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-content")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-content/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ echo 'content' | http POST 'http://localhost:8080/'" + + " 'Accept:application/json'")))); + } + + @Test + public void httpieSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-query-string/httpie-request.adoc"), + is(snippet(asciidoctor()) + .withContents(codeBlock(asciidoctor(), "bash").content("$ http " + + "--form POST 'http://localhost:8080/?foo=bar' " + + "'Accept:application/json' 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From 6ea6dcf99663edaff1bdb82a64f2086f2fcb3546 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Mar 2016 17:51:48 +0000 Subject: [PATCH 0237/1059] Fix test failures on Windows caused by different new line characters --- .../cli/HttpieRequestSnippetTests.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 6194fef91..3fd92c383 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -236,8 +236,9 @@ public void requestWithHeaders() throws IOException { @Test public void multipartPostWithNoSubmittedFileName() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'metadata'@<(echo '{\"description\": \"foo\"}')"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); this.snippet.expectHttpieRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -252,8 +253,9 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { @Test public void multipartPostWithContentType() throws IOException { // httpie does not yet support manually set content type by part - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png'"); this.snippet.expectHttpieRequest("multipart-post-with-content-type") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -268,8 +270,9 @@ public void multipartPostWithContentType() throws IOException { @Test public void multipartPost() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png'"); this.snippet.expectHttpieRequest("multipart-post") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -283,8 +286,10 @@ public void multipartPost() throws IOException { @Test public void multipartPostWithParameters() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado' 'b=banana'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + + " 'b=banana'"); this.snippet.expectHttpieRequest("multipart-post-with-parameters") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() From 488d36d4bfea50ea0bd28026d10e7a3c5a0120c4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 10:47:11 +0100 Subject: [PATCH 0238/1059] Do not double-encode URIs when they are being modified Previously, when UriModifyingOperationPreprocessor modified a URI it would always encode it. However, the URIs being modified will always have been encoded, resulting in double-encoding of the URI. This commit updates UriModifyingOperationPreprocessor to tell UriComponentsBuilder that the components have already been encoded. This ensures that they are not encoded for a second time when the URI is being built. Closes gh-211 --- .../preprocess/UriModifyingOperationPreprocessor.java | 2 +- .../UriModifyingOperationPreprocessorTests.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index b06530b6c..9063ac948 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -140,7 +140,7 @@ public OperationRequest preprocess(OperationRequest request) { modifiedHeaders.set(HttpHeaders.HOST, this.host); } return this.contentModifyingDelegate.preprocess( - new OperationRequestFactory().create(uriBuilder.build().toUri(), + new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), request.getContent(), modifiedHeaders, request.getParameters(), modify(request.getParts()))); } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 30677718b..60bda2db8 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -307,6 +307,16 @@ public void urisInRequestPartContentCanBeModified() { is(equalTo("The uri 'http://api.example.com:12345' should be used"))); } + @Test + public void modifiedUriDoesNotGetDoubleEncoded() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://localhost:12345?foo=%7B%7D")); + assertThat(processed.getUri(), + is(equalTo(URI.create("https://localhost:12345?foo=%7B%7D")))); + + } + private OperationRequest createRequestWithUri(String uri) { return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], new HttpHeaders(), new Parameters(), From cb39af5163ce1899129baf479e1f606c3348fe21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:12:15 +0100 Subject: [PATCH 0239/1059] Improve the formatting of the default Markdown path parameters snippet Add the required blank line between the table "title" and the table itself. Without this blank line the table wasn't not formatted correctly. The "title" has also been wrapped in back ticks to improve its formatting in the generated HTML. Closes gh-212 --- .../markdown/default-path-parameters.snippet | 3 ++- .../restdocs/request/PathParametersSnippetTests.java | 12 +++++++++--- .../restdocs/test/SnippetMatchers.java | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet index f5f1daaee..f1811879f 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet @@ -1,4 +1,5 @@ -{{path}} +`{{path}}` + Parameter | Description --------- | ----------- {{#parameters}} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 7bee0c142..1b50d3d17 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -49,7 +50,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) @@ -61,7 +62,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { this.snippet.expectPathParameters("ignored-path-parameter").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -74,7 +75,7 @@ public void ignoredPathParameter() throws IOException { public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") .withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))).document( @@ -132,4 +133,9 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { .build()); } + private String getTitle() { + return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" + : "`/{a}/{b}`"; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index a7875b76d..bb0c146cd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -345,6 +345,7 @@ private MarkdownTableMatcher(String title, String... columns) { super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine(title); + this.addLine(""); } String header = StringUtils .collectionToDelimitedString(Arrays.asList(columns), " | "); From 05f738d12c04891d42f13a90fdf2649765f6750b Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 4 Mar 2016 17:54:01 +0900 Subject: [PATCH 0240/1059] Fix typo --- .../restdocs/templates/mustache/MustacheTemplateEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index fee0f6af9..8079060ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -52,7 +52,7 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) /** * Creates a new {@code MustacheTemplateEngine} that will use the given - * {@code tempalteResourceResolver} to resolve templates and the given + * {@code templateResourceResolver} to resolve templates and the given * {@code compiler} to compile them. * * @param templateResourceResolver the resolver to use From 547c4f3fb14777014103faa9f3d3e29c82d19742 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:32:15 +0100 Subject: [PATCH 0241/1059] Simplify documentation of Asciidoctor Maven plugin's phase Previously the phase was documented as `package` by default with instructions to change it to `prepare-package` if you want to include the documentation in the project's package. This led to some confusion and sometimes the step to change it to `prepare-package` was missed. This commit updates the documentation to recommend that `prepare-package` is always used. It works equally well for both cases (packaging the documentation or not) and avoids the possible confusion described above. Closes gh-218 --- docs/src/docs/asciidoc/getting-started.adoc | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0232564be..4a12eef42 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -134,7 +134,7 @@ described below. generate-docs - package <6> + prepare-package <6> process-asciidoc @@ -159,9 +159,8 @@ described below. <4> Add the Asciidoctor plugin <5> Define an attribute named `snippets` that can be used when including the generated snippets in your documentation. -<6> [[getting-started-build-configuration-maven-plugin-phase]] If you want to -<> in your -project's jar you should use the `prepare-package` phase. +<6> Using `prepare-package` allows the documentation to be + <>. [[getting-started-build-configuration-maven-packaging]] ==== Packaging the documentation @@ -170,10 +169,8 @@ You may want to package the generated documentation in your project's jar file, example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as static content] by Spring Boot. -First, configure the Asciidoctor plugin so that it runs in the `prepare-package` phase, as -<>. Now configure -Maven's resources plugin to copy the generated documentation into a location where it'll -be included in the project's jar: +Configure Maven's resources plugin to copy the generated documentation into a location +where it'll be included in the project's jar: [source,xml,indent=0] ---- From 83e8b92c3bf4f21be895b3cbede19dab67eedea2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:55:52 +0100 Subject: [PATCH 0242/1059] Upgrade to Gradle 2.12 --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 4 ++-- gradlew.bat | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 2519 zcmZWq3s6%>6y3ap5G9EaCBl3x(7|aNRC@L0#DcC_; z3Tw3%JBXm5r9&XGs6|EjiG-huHC_j1vRZ-3oKH?YN^>Xh-d@Gn9WxLNe-!HL2A2C|q6eYgY6&L?;NIB9bvo?pxcKtht%b!X{CZ?8fY-8V2B$^vPz($g-g*=?MY^7ZRVijR%{Qm@_qJq5sN z;iS<{E8U~2<7`S!O$0k_ziv63>~gk~#m3Ktvb?amLcWYKr8&Cx;40LumFTqvFt~dz zw5+#yXQwGcr*>Lk{agWa|@ZT&(oX44`mfIQ+yz>WZzf^okh^eZtr&P^T2 zqh|5RIc758oXfP&CH7-na&(L)_B{?GG@*)?iG3K=XRtsZcYRkgJ}YKRDPr}Y*M5SI zMt!C=)L!NnwPVKoLfyCA#vH2Wgv=OYnYz_yTgz(`N1twedetxgX2!K)41WyiGB*J8G zkpgaL6l1bMvT0QTQXWC8&Q-RNVsem_hE)w>e4)Tg4Ky`sit(WaXM$Nb($su8;WkZF0n>RJUs9-9YZ`Eznux zRFLV-nlgsPO*9d{fCPT<$M|5AJ#PM;4zpdyP(^y_yNx{vmCf#$lb}fWR?h?AGd=*` zoJmq(MsonsZfmBg^mRzv8VXuu*xcm3iE_A$e=8EcigbV*Tg2~Nm?6-7g3hb8QcrlIduF~Qedr76c4=ojPi;s3WDbEraAOtY>)>-7dzx0K^r z7K;+>p*nUB9G?)9w9?e9cvw>94A;lY;h;gRU_=J&C9eZe>48Ydog)IaA`vU7ZMDZl z&gI1+9@4;S%`$kgjVM}el~=bTwR_0cA`bCv3Z8x}#dLD|zRf*Oj}dtrx)zsl$ahn5 z4Hmm>CY9~>v~DOwyZML&07nklA`N>DP7}jln~9C1nuyMFRM^S))^sof+xWT7nzXp1 zji#Pw!JTn)**=len*jt2sl8-Pu zxU^k{8y7j_@9n*gwcWs9HmtE)}tz&_z>2 k=SYvvn=#vf;MWiyP>1np-Hup~`v)^6sL#9)gSx%{1J}&t-T(jq delta 2539 zcmZWq2~bl<7~VWWh#I0q3E?$BgaRTSA%JpdJu6n^Qt-Ay2ZRCY0SFef5-d6bEy`9$ z99p%AVhROJl-i<#K)3^_c#O6l^`MG?c7{r;*nPWC;-i^)*?r%B|F_@2`|rLzg(iCn zO~k?eCj8L=2!$Zqf12L`v7^uXNZmid1ryhz}I5zNyHK8|Q}Che=~ zxP`8~d$^+=OC?YQ%H%&L&}P#@0%@f}iGW~J#5Y+?XffTCV7}5^7Bk4g%n0VJ2pdm& z>F#4#Uaptr7=jfq7#J;P{M<5FY(FLdKqrQ|d>Z!G31EO;sJ#}b5lEQvSVio99S?vN zCIEN=sMh;vo5L>hh)T}NP$ACWI%(^(5R`uqe7<2mEexC*tn>&`mg`h!%2ldmTawMi zAxf3f;}KjO?qZ&dWOX;L%`>0?8h#mWx63oPZBwmc+*f^7ODy^4YVKD%RxSyfwlB3* zy2^KB+tI`hj7s^6<8$7W=*suCyx;75>;Ovs(H>{q=VbU+z9@ zKO^(rvr|`-V@}?C**#a&q;$FI5#C-pUmvi$N%NvOHNn4Vk+9gNx4>Y1C&S}W{;Pz2 z<%ia<)ud(=DbCl5?;g$$3Q6=?Hc;!6*F0!3^m6OJpZ5=>HK#R;=gT+d9KPTf7nq_8 zn=^BN{Dz|kO2dcd99t99b`ZYte#kf7=GQRu5*09LV%&KqRhSy#%4a&-(-C_q+=CvG z-2_rZ4N+odu&N|Lb$|k3w<-L-Y^rv3Oa-B8#c_E&!aWghDu=Yi{rv|mf;)$7Cd6cKp#rX{6Jc_z`KODRLLEex23OfejLAV_8kW_H$i`f% zrK!Pj81JujA(%}aO-+$vEU->Suvmm$lA)!uheI*}{HczOzld;|0;ML019cL@71h(! zR(BjfyWR<#T713o!>h=>gA9O4oD9;vSW{;3Xgy5?E+B#Y1sLzGcfgBpyaq=uEQCtE z8|HAYLgOqtbs|jH`w{IHJx%RgiL`AXzd?fIhA&4H!rlBX=-HqeaUUi%c;hn#D;j8C z9Bgf{C9k~=G<75bDh<}KBZ7vr8$|>UXrw8}NEnf853?I5D42t^Z2l;lVzfnnqb-_c z^a*roizrMCj*`PS?*z=7Xli^6#?zb4;OZDD>}?fcp>7BBmwxE-_eWxG9HFUiEHnKL8L##|o7Uu^0?s!F!08{PVoCWV@ z%`)yyRx3>%%E6+Ittuiogs_FV2%7}=wG&4h#(s3A5asHEa^;@CzT-G}f14einR6L) zX8z;EuyvOrk?Z?+8dDM5ogRdSqpN@(1As3lP!FdYmJnw<4K%f@1f@D{c&*M=mnR-Z z&NOJdxciz}Hawxc-5$Gh<}Vgmqjia9QsGRp{R|?@p(uMSvo@H#pi}zzDO$iC$Pu?j zZ)&mfjrK`)jCBZn^O@Ctja0oks&Z)Cv6xto?Vzc?^EhtWFA}CM2;PkFMO7Hz-zI?K T&WTu)`=v38>eP2&V5irA_39c# diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c9568aee..0b95e5d4b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:15:12 GMT 2016 +#Mon Apr 04 11:55:32 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/gradlew b/gradlew index 97fac783e..9d82f7891 100755 --- a/gradlew +++ b/gradlew @@ -56,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa..5f192121e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From feb2f352f69200c0b4fd098c1d8017b065c7da69 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 13:26:41 +0100 Subject: [PATCH 0243/1059] Fix compiler warnings --- build.gradle | 4 +++ .../mockmvc/CustomDefaultSnippets.java | 1 + .../com/example/mockmvc/CustomEncoding.java | 2 +- .../com/example/mockmvc/CustomFormat.java | 2 +- .../mockmvc/CustomUriConfiguration.java | 2 +- .../mockmvc/EveryTestPreprocessing.java | 1 - .../ExampleApplicationTestNgTests.java | 5 ++-- .../mockmvc/ExampleApplicationTests.java | 7 +++-- .../example/mockmvc/ParameterizedOutput.java | 2 +- .../restassured/CustomDefaultSnippets.java | 1 + .../example/restassured/CustomEncoding.java | 2 +- .../com/example/restassured/CustomFormat.java | 4 +-- .../restassured/EveryTestPreprocessing.java | 1 - .../ExampleApplicationTestNgTests.java | 1 + .../restassured/ExampleApplicationTests.java | 2 +- .../restassured/ParameterizedOutput.java | 2 +- .../snippet/StandardWriterResolver.java | 1 + .../RestDocumentationConfigurerTests.java | 29 +++++++++++-------- ...tationContextPlaceholderResolverTests.java | 17 +++++++---- .../snippet/StandardWriterResolverTests.java | 22 +++++++------- .../restdocs/test/OperationBuilder.java | 12 ++++++-- .../mockmvc/MockMvcRestDocumentation.java | 4 +-- 22 files changed, 77 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index c6637e331..f1d171285 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,10 @@ subprojects { } } + compileJava { + options.compilerArgs = [ '-Xlint:deprecation', '-Xlint:-options', '-Werror' ] + } + } configure(subprojects - project(":docs")) { subproject -> diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 4fb2990f8..6c974927c 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -37,6 +37,7 @@ public class CustomDefaultSnippets { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 9352ca95e..0351216aa 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,6 +34,7 @@ public class CustomEncoding { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 2415bd40c..b0cae3b18 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -36,6 +35,7 @@ public class CustomFormat { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index b091199a8..bc3e0e75f 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,6 +34,7 @@ public class CustomUriConfiguration { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 85e7e9979..18799410e 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java index 0018c467b..fac8c5ca8 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -32,12 +32,13 @@ public class ExampleApplicationTestNgTests { public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( "target/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] + private MockMvc mockMvc; + @Autowired private WebApplicationContext context; - private MockMvc mockMvc; - @BeforeMethod public void setUp(Method method) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index a27725f04..21afd53d3 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; @@ -31,12 +32,14 @@ public class ExampleApplicationTests { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); + + @SuppressWarnings("unused") // tag::setup[] + private MockMvc mockMvc; + @Autowired private WebApplicationContext context; - private MockMvc mockMvc; - @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index c5e6a362a..d39102bff 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -32,6 +31,7 @@ public class ParameterizedOutput { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + @SuppressWarnings("unused") private MockMvc mockMvc; private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 57c65d360..27512e649 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -32,6 +32,7 @@ public class CustomDefaultSnippets { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build"); + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 09039a448..6e5c6e867 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -31,6 +30,7 @@ public class CustomEncoding { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 36ba445d4..c0d8adb7e 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -31,7 +30,8 @@ public class CustomFormat { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); - + + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index a4ea45880..e143bcc26 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.restassured.RestDocumentationFilter; import com.jayway.restassured.RestAssured; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index a28e54aec..aa0a57966 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -32,6 +32,7 @@ public class ExampleApplicationTestNgTests { private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index 33840cb70..c7b8d774a 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -32,6 +31,7 @@ public class ExampleApplicationTests { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 29c24835a..88db51d8f 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -33,6 +32,7 @@ public class ParameterizedOutput { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") private RequestSpecification spec; // tag::parameterized-output[] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 9faf267dd..479407ea0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -96,6 +96,7 @@ public Writer resolve(String operationName, String snippetName, } @Override + @Deprecated public void setEncoding(String encoding) { this.encoding = encoding; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 7198ffc85..a629283f2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -23,6 +23,7 @@ import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.cli.CurlRequestSnippet; @@ -57,9 +58,8 @@ public class RestDocumentationConfigurerTests { @SuppressWarnings("unchecked") @Test public void defaultConfiguration() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.apply(configuration, context); + this.configurer.apply(configuration, createContext()); assertThat(configuration, hasEntry(equalTo(TemplateEngine.class.getName()), instanceOf(MustacheTemplateEngine.class))); assertThat(configuration, hasEntry(equalTo(WriterResolver.class.getName()), @@ -86,30 +86,29 @@ public void defaultConfiguration() { @Test public void customTemplateEngine() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); TemplateEngine templateEngine = mock(TemplateEngine.class); - this.configurer.templateEngine(templateEngine).apply(configuration, context); + this.configurer.templateEngine(templateEngine).apply(configuration, + createContext()); assertThat(configuration, Matchers.hasEntry( TemplateEngine.class.getName(), templateEngine)); } @Test public void customWriterResolver() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); WriterResolver writerResolver = mock(WriterResolver.class); - this.configurer.writerResolver(writerResolver).apply(configuration, context); + this.configurer.writerResolver(writerResolver).apply(configuration, + createContext()); assertThat(configuration, Matchers.hasEntry( WriterResolver.class.getName(), writerResolver)); } @Test public void customDefaultSnippets() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()) - .apply(configuration, context); + .apply(configuration, createContext()); assertThat(configuration, hasEntry( equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), @@ -122,10 +121,9 @@ public void customDefaultSnippets() { @Test public void customSnippetEncoding() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withEncoding("ISO 8859-1").apply(configuration, - context); + createContext()); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -135,10 +133,9 @@ public void customSnippetEncoding() { @Test public void customTemplateFormat() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()) - .apply(configuration, context); + .apply(configuration, createContext()); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -147,6 +144,14 @@ public void customTemplateFormat() { is(equalTo(TemplateFormats.markdown()))); } + private RestDocumentationContext createContext() { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); + manualRestDocumentation.beforeTest(null, null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + private static final class TestRestDocumentationConfigurer extends RestDocumentationConfigurer { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index def767471..83f96e611 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -18,6 +18,7 @@ import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -70,17 +71,23 @@ public void camelCaseClassName() throws Exception { @Test public void stepCount() throws Exception { - assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("0")); + assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("1")); } private PlaceholderResolver createResolver() { - return new RestDocumentationContextPlaceholderResolver( - new RestDocumentationContext(getClass(), null, null)); + return createResolver(null); } private PlaceholderResolver createResolver(String methodName) { - return new RestDocumentationContextPlaceholderResolver( - new RestDocumentationContext(getClass(), methodName, null)); + return new RestDocumentationContextPlaceholderResolver(createContext(methodName)); + } + + private RestDocumentationContext createContext(String methodName) { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); + manualRestDocumentation.beforeTest(getClass(), methodName); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 56c2b573a..af5ccd149 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -20,11 +20,11 @@ import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; @@ -42,18 +42,12 @@ public class StandardWriterResolverTests { private final StandardWriterResolver resolver = new StandardWriterResolver( this.placeholderResolver, "UTF-8", asciidoctor()); - @Test - public void noConfiguredOutputDirectoryAndRelativeInput() { - assertThat(this.resolver.resolveFile("foo", "bar.txt", - new RestDocumentationContext(null, null, null)), is(nullValue())); - } - @Test public void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); assertThat( this.resolver.resolveFile(absolutePath, "bar.txt", - new RestDocumentationContext(null, null, null)), + createContext(absolutePath)), is(new File(absolutePath, "bar.txt"))); } @@ -62,7 +56,7 @@ public void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); assertThat( this.resolver.resolveFile("bar", "baz.txt", - new RestDocumentationContext(null, null, outputDir)), + createContext(outputDir.getAbsolutePath())), is(new File(outputDir, "bar/baz.txt"))); } @@ -72,8 +66,16 @@ public void configuredOutputAndAbsoluteInput() { String absolutePath = new File("bar").getAbsolutePath(); assertThat( this.resolver.resolveFile(absolutePath, "baz.txt", - new RestDocumentationContext(null, null, outputDir)), + createContext(outputDir.getAbsolutePath())), is(new File(absolutePath, "baz.txt"))); } + private RestDocumentationContext createContext(String outputDir) { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + outputDir); + manualRestDocumentation.beforeTest(getClass(), null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index a9bc7d3cf..86b426a30 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -27,6 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -96,8 +97,7 @@ public Operation build() { new MustacheTemplateEngine( new StandardTemplateResourceResolver(this.templateFormat))); } - RestDocumentationContext context = new RestDocumentationContext(null, null, - this.outputDirectory); + RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( @@ -110,6 +110,14 @@ public Operation build() { this.responseBuilder.buildResponse(), this.attributes); } + private RestDocumentationContext createContext() { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + this.outputDirectory.getAbsolutePath()); + manualRestDocumentation.beforeTest(null, null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + /** * Basic builder API for creating an {@link OperationRequest}. */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 1fb3c33bf..d26d9e105 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.mockmvc; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; @@ -32,7 +31,6 @@ * * @author Andy Wilkinson */ -@SuppressWarnings("deprecation") public abstract class MockMvcRestDocumentation { private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); @@ -55,7 +53,7 @@ private MockMvcRestDocumentation() { */ @Deprecated public static MockMvcRestDocumentationConfigurer documentationConfiguration( - RestDocumentation restDocumentation) { + org.springframework.restdocs.RestDocumentation restDocumentation) { return documentationConfiguration( (RestDocumentationContextProvider) restDocumentation); } From 8b039e5f816aa4fa40488baa7cf80ea0bd9bda03 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 17:29:40 +0100 Subject: [PATCH 0244/1059] Add support for reusing a snippet to document common elements This commit updates all of the Snippet implementations that take one or more descriptors to provide an and method that can be used to create a new Snippet that has additional descriptors. Closes gh-168 --- .../docs/asciidoc/documenting-your-api.adoc | 39 +++++++++++++++++ .../test/java/com/example/SnippetReuse.java | 34 +++++++++++++++ .../example/mockmvc/MockMvcSnippetReuse.java | 43 +++++++++++++++++++ .../restassured/RestAssuredSnippetReuse.java | 43 +++++++++++++++++++ .../restdocs/headers/HeaderDocumentation.java | 11 ++--- .../headers/RequestHeadersSnippet.java | 19 +++++++- .../headers/ResponseHeadersSnippet.java | 19 +++++++- .../restdocs/http/HttpDocumentation.java | 12 +++--- .../hypermedia/HypermediaDocumentation.java | 12 +++--- .../restdocs/hypermedia/LinksSnippet.java | 15 +++++++ .../payload/PayloadDocumentation.java | 12 +++--- .../payload/RequestFieldsSnippet.java | 18 +++++++- .../payload/ResponseFieldsSnippet.java | 18 +++++++- .../request/AbstractParametersSnippet.java | 13 ++++++ .../request/PathParametersSnippet.java | 16 +++++++ .../request/RequestDocumentation.java | 13 +++--- .../request/RequestParametersSnippet.java | 16 +++++++ .../headers/RequestHeadersSnippetTests.java | 23 ++++++++++ .../headers/ResponseHeadersSnippetTests.java | 21 +++++++++ .../hypermedia/LinksSnippetTests.java | 12 ++++++ .../payload/RequestFieldsSnippetTests.java | 16 +++++++ .../payload/ResponseFieldsSnippetTests.java | 21 +++++++++ .../request/PathParametersSnippetTests.java | 13 ++++++ .../RequestParametersSnippetTests.java | 12 ++++++ 24 files changed, 435 insertions(+), 36 deletions(-) create mode 100644 docs/src/test/java/com/example/SnippetReuse.java create mode 100644 docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java create mode 100644 docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index f036275c9..88313f978 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -445,6 +445,45 @@ When documenting HTTP Headers, the test will fail if a documented header is not the request or response. + +[[documenting-your-api-reusing-snippets]] +=== Reusing snippets + +It's common for an API that's being documented to have some features that are common +across several of its resources. To avoid repetition when documenting such resources a +`Snippet` configured with the common elements can be reused. + +First, create the `Snippet` that describes the common elements. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/SnippetReuse.java[tags=field] +---- + +Second, use this snippet and add further descriptors that are resource-specific. For +example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/MockMvcSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific + to the resource that is being documented. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RestAssuredSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific + to the resource that is being documented. + +The result of the example is that links with the rels `first`, `last`, `next`, `previous`, +`alpha`, and `bravo` are all documented. + + + [[documenting-your-api-constraints]] === Documenting constraints diff --git a/docs/src/test/java/com/example/SnippetReuse.java b/docs/src/test/java/com/example/SnippetReuse.java new file mode 100644 index 000000000..6ab40ddb6 --- /dev/null +++ b/docs/src/test/java/com/example/SnippetReuse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2016 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 com.example; + +import org.springframework.restdocs.hypermedia.LinksSnippet; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; + +public class SnippetReuse { + + // tag::field[] + protected final LinksSnippet pagingLinks = links( + linkWithRel("first").optional().description("The first page of results"), + linkWithRel("last").optional().description("The last page of results"), + linkWithRel("next").optional().description("The next page of results"), + linkWithRel("prev").optional().description("The previous page of results")); + // end::field[] + +} diff --git a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java new file mode 100644 index 000000000..34dc3ae1b --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 com.example.mockmvc; + +import com.example.SnippetReuse; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class MockMvcSnippetReuse extends SnippetReuse { + + private MockMvc mockMvc; + + public void documentation() throws Exception { + // tag::use[] + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("example", this.pagingLinks.and( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java new file mode 100644 index 000000000..c792a7fbc --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 com.example.restassured; + +import com.example.SnippetReuse; +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RestAssuredSnippetReuse extends SnippetReuse { + + private RequestSpecification spec; + + public void documentation() throws Exception { + // tag::use[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("example", this.pagingLinks.and( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + .get("/").then().assertThat().statusCode(is(200)); + // end::use[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 85800ecff..1eb4f0f9e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -55,7 +55,7 @@ public static HeaderDescriptor headerWithName(String name) { * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static Snippet requestHeaders(HeaderDescriptor... descriptors) { + public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descriptors) { return new RequestHeadersSnippet(Arrays.asList(descriptors)); } @@ -72,7 +72,7 @@ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static Snippet requestHeaders(Map attributes, + public static RequestHeadersSnippet requestHeaders(Map attributes, HeaderDescriptor... descriptors) { return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); } @@ -88,7 +88,8 @@ public static Snippet requestHeaders(Map attributes, * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static Snippet responseHeaders(HeaderDescriptor... descriptors) { + public static ResponseHeadersSnippet responseHeaders( + HeaderDescriptor... descriptors) { return new ResponseHeadersSnippet(Arrays.asList(descriptors)); } @@ -106,7 +107,7 @@ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static Snippet responseHeaders(Map attributes, + public static ResponseHeadersSnippet responseHeaders(Map attributes, HeaderDescriptor... descriptors) { return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index 83dd6b6d5..ecdc933a6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,8 @@ package org.springframework.restdocs.headers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +29,7 @@ * A {@link Snippet} that documents the headers in a request. * * @author Andreas Evers + * @author Andy Wilkinson * @see HeaderDocumentation#requestHeaders(HeaderDescriptor...) * @see HeaderDocumentation#requestHeaders(Map, HeaderDescriptor...) */ @@ -60,4 +63,18 @@ protected Set extractActualHeaders(Operation operation) { return operation.getRequest().getHeaders().keySet(); } + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.getHeaderDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestHeadersSnippet(combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index 979083c42..1a4171679 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,8 @@ package org.springframework.restdocs.headers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +29,7 @@ * A {@link Snippet} that documents the headers in a response. * * @author Andreas Evers + * @author Andy Wilkinson * @see HeaderDocumentation#responseHeaders(HeaderDescriptor...) * @see HeaderDocumentation#responseHeaders(Map, HeaderDescriptor...) */ @@ -60,4 +63,18 @@ protected Set extractActualHeaders(Operation operation) { return operation.getResponse().getHeaders().keySet(); } + /** + * Returns a new {@code ResponseHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.getHeaderDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new ResponseHeadersSnippet(combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index f1bc8b389..5d872fe28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,8 +18,6 @@ import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API's HTTP requests. * @@ -38,7 +36,7 @@ private HttpDocumentation() { * * @return the snippet that will document the HTTP request */ - public static Snippet httpRequest() { + public static HttpRequestSnippet httpRequest() { return new HttpRequestSnippet(); } @@ -50,7 +48,7 @@ public static Snippet httpRequest() { * @param attributes the attributes * @return the snippet that will document the HTTP request */ - public static Snippet httpRequest(Map attributes) { + public static HttpRequestSnippet httpRequest(Map attributes) { return new HttpRequestSnippet(attributes); } @@ -60,7 +58,7 @@ public static Snippet httpRequest(Map attributes) { * * @return the snippet that will document the HTTP response */ - public static Snippet httpResponse() { + public static HttpResponseSnippet httpResponse() { return new HttpResponseSnippet(); } @@ -72,7 +70,7 @@ public static Snippet httpResponse() { * @param attributes the attributes * @return the snippet that will document the HTTP response */ - public static Snippet httpResponse(Map attributes) { + public static HttpResponseSnippet httpResponse(Map attributes) { return new HttpResponseSnippet(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index d5ddaf0b4..c2f11b75d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,8 +19,6 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API that utilizes Hypermedia. * @@ -59,7 +57,7 @@ public static LinkDescriptor linkWithRel(String rel) { * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkDescriptor... descriptors) { + public static LinksSnippet links(LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors)); } @@ -83,7 +81,7 @@ public static Snippet links(LinkDescriptor... descriptors) { * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(Map attributes, + public static LinksSnippet links(Map attributes, LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors), attributes); @@ -107,7 +105,7 @@ public static Snippet links(Map attributes, * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkExtractor linkExtractor, + public static LinksSnippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } @@ -132,7 +130,7 @@ public static Snippet links(LinkExtractor linkExtractor, * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkExtractor linkExtractor, + public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 771e2e7a8..7c684ba9f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -170,4 +171,18 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor return model; } + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link + * extractor and attributes, and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public LinksSnippet and(LinkDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.descriptorsByRel.values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new LinksSnippet(this.linkExtractor, combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 097e1c47c..f2391277b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,8 +19,6 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API's request and response payloads. * @@ -117,7 +115,7 @@ public static FieldDescriptor fieldWithPath(String path) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet requestFields(FieldDescriptor... descriptors) { + public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { return new RequestFieldsSnippet(Arrays.asList(descriptors)); } @@ -142,7 +140,7 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet requestFields(Map attributes, + public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } @@ -167,7 +165,7 @@ public static Snippet requestFields(Map attributes, * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet responseFields(FieldDescriptor... descriptors) { + public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } @@ -192,7 +190,7 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet responseFields(Map attributes, + public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 354a0283e..fdc3e609e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,6 +17,8 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -66,4 +68,18 @@ protected byte[] getContent(Operation operation) throws IOException { return operation.getRequest().getContent(); } + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 82015ef8f..14d5dd555 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,6 +17,8 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -66,4 +68,18 @@ protected byte[] getContent(Operation operation) throws IOException { return operation.getResponse().getContent(); } + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 0be930228..21d900fe8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -122,11 +122,24 @@ protected abstract void verificationFailed(Set undocumentedParameters, * {@link ParameterDescriptor#getName()}. * * @return the map of path descriptors + * @deprecated since 1.1.0 in favor of {@link #getParameterDescriptors()} */ + @Deprecated protected final Map getFieldDescriptors() { return this.descriptorsByName; } + /** + * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will + * be used to generate the documentation key by their + * {@link ParameterDescriptor#getName()}. + * + * @return the map of path descriptors + */ + protected final Map getParameterDescriptors() { + return this.descriptorsByName; + } + /** * Returns a model for the given {@code descriptor}. * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 77cc1b4d7..01ef25e41 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.request; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -122,4 +124,18 @@ protected void verificationFailed(Set undocumentedParameters, throw new SnippetException(message); } + /** + * Returns a new {@code PathParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getParameterDescriptors().values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new PathParametersSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index e0aa2f4ba..6fa64c9bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -20,7 +20,6 @@ import java.util.Map; import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -61,7 +60,8 @@ public static ParameterDescriptor parameterWithName(String name) { * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static Snippet pathParameters(ParameterDescriptor... descriptors) { + public static PathParametersSnippet pathParameters( + ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors)); } @@ -84,7 +84,7 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static Snippet pathParameters(Map attributes, + public static PathParametersSnippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } @@ -107,7 +107,8 @@ public static Snippet pathParameters(Map attributes, * @return the snippet * @see OperationRequest#getParameters() */ - public static Snippet requestParameters(ParameterDescriptor... descriptors) { + public static RequestParametersSnippet requestParameters( + ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors)); } @@ -131,8 +132,8 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { * @return the snippet that will document the parameters * @see OperationRequest#getParameters() */ - public static Snippet requestParameters(Map attributes, - ParameterDescriptor... descriptors) { + public static RequestParametersSnippet requestParameters( + Map attributes, ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 5b1f9ba81..e85423fc1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.request; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,4 +86,18 @@ protected Set extractActualParameters(Operation operation) { return operation.getRequest().getParameters().keySet(); } + /** + * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getParameterDescriptors().values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestParametersSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index fb13925a4..594415628 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -143,4 +143,27 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .header("Accept", "*/*").build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestHeaders("additional-descriptors") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Accept", "two").row("Accept-Encoding", "three") + .row("Accept-Language", "four").row("Cache-Control", "five") + .row("Connection", "six")); + HeaderDocumentation + .requestHeaders(headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four")) + .and(headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 6e16402ba..d3f2ffbd5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -133,4 +133,25 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .header("Etag", "lskjadldj3ii32l2ij23").build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectResponseHeaders("additional-descriptors") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Content-Type", "two").row("Etag", "three") + .row("Cache-Control", "five").row("Vary", "six")); + HeaderDocumentation + .responseHeaders(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), + headerWithName("Etag").description("three")) + .and(headerWithName("Cache-Control").description("five"), + headerWithName("Vary") + .description("six")) + .document(operationBuilder("additional-descriptors").response() + .header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0").header("Vary", "User-Agent") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b04109765..b78b3b678 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -132,4 +132,16 @@ public void linksWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectLinks("additional-descriptors") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") + .row("b", "two")); + HypermediaDocumentation + .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + new LinkDescriptor("a").description("one")) + .and(new LinkDescriptor("b").description("two")) + .document(operationBuilder("additional-descriptors").build()); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 948383671..195ed58a8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -160,4 +160,20 @@ public void xmlRequestFields() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestFields("additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a.b", "Number", "one").row("a.c", "String", "two") + .row("a", "Object", "three")); + + PayloadDocumentation + .requestFields(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 4d996699b..8e33fadad 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -211,4 +211,25 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectResponseFields("additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("id", "Number", "one").row("date", "String", "two") + .row("assets", "Array", "three").row("assets[]", "Object", "four") + .row("assets[].id", "Number", "five") + .row("assets[].name", "String", "six")); + PayloadDocumentation + .responseFields(fieldWithPath("id").description("one"), + fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three")) + .and(fieldWithPath("assets[]").description("four"), + fieldWithPath("assets[].id").description("five"), + fieldWithPath("assets[].name").description("six")) + .document(operationBuilder("additional-descriptors").response() + .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 1b50d3d17..fb28e1cd5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -133,6 +133,19 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectPathParameters("additional-descriptors").withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("a", "one").row("b", "two")); + RequestDocumentation.pathParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); + } + private String getTitle() { return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" : "`/{a}/{b}`"; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 204806522..4e533fd97 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -128,4 +128,16 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestParameters("additional-descriptors") + .withContents(tableWithHeader("Parameter", "Description").row("a", "one") + .row("b", "two")); + RequestDocumentation.requestParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); + } + } From bab779ac919f98b668cf0e79f5f2cbb6b33ae64a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 17:50:56 +0100 Subject: [PATCH 0245/1059] Disable wrapping in Asciidoctor HTTP request and response snippets The HTTP request and response snippets are intended to be an exact representation of the HTTP request or response which would not wrap. This commit disables wrapping by adding the nowrap option to the default templates for these two snippets. Closes gh-204 --- .../restdocs/templates/asciidoctor/default-http-request.snippet | 2 +- .../templates/asciidoctor/default-http-response.snippet | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet index a3e529aed..9893e2a03 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet @@ -1,4 +1,4 @@ -[source,http] +[source,http,options="nowrap"] ---- {{method}} {{path}} HTTP/1.1 {{#headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet index fe095d36e..90a25e851 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet @@ -1,4 +1,4 @@ -[source,http] +[source,http,options="nowrap"] ---- HTTP/1.1 {{statusCode}} {{statusReason}} {{#headers}} From 2da1a0a979f824cf78539bf3075fc5608c07c44d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 19:36:34 +0100 Subject: [PATCH 0246/1059] Update tests following nowrap changes made in bab779a Closes gh-204 --- .../restdocs/test/SnippetMatchers.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index bb0c146cd..34d84a5eb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -72,7 +72,7 @@ public static HttpRequestMatcher httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { if ("adoc".equals(format.getFileExtension())) { return new HttpRequestMatcher(requestMethod, uri, - new AsciidoctorCodeBlockMatcher<>("http"), 3); + new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); } return new HttpRequestMatcher(requestMethod, uri, new MarkdownCodeBlockMatcher<>("http"), 2); @@ -82,7 +82,7 @@ public static HttpResponseMatcher httpResponse(TemplateFormat format, HttpStatus status) { if ("adoc".equals(format.getFileExtension())) { return new HttpResponseMatcher(status, - new AsciidoctorCodeBlockMatcher<>("http"), 3); + new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); } return new HttpResponseMatcher(status, new MarkdownCodeBlockMatcher<>("http"), 2); } @@ -90,7 +90,7 @@ public static HttpResponseMatcher httpResponse(TemplateFormat format, @SuppressWarnings({ "rawtypes" }) public static CodeBlockMatcher codeBlock(TemplateFormat format, String language) { if ("adoc".equals(format.getFileExtension())) { - return new AsciidoctorCodeBlockMatcher(language); + return new AsciidoctorCodeBlockMatcher(language, null); } return new MarkdownCodeBlockMatcher(language); } @@ -180,9 +180,10 @@ public T content(String content) { public static class AsciidoctorCodeBlockMatcher> extends CodeBlockMatcher { - protected AsciidoctorCodeBlockMatcher(String language) { + protected AsciidoctorCodeBlockMatcher(String language, String options) { super(TemplateFormats.asciidoctor()); - this.addLine("[source," + language + "]"); + this.addLine("[source," + language + + (options == null ? "" : ",options=\"" + options + "\"") + "]"); this.addLine("----"); this.addLine("----"); } From e5aa5ba7283329f759faaf057d18e4fcafcd6e67 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 10:07:41 +0100 Subject: [PATCH 0247/1059] Make it easier to write a preprocessor that only changes req or resp The commit introduces OperationPreprocessorAdapter, an abstract implementation of OperationProcessor that returns to request and response as-is. OperationPreprocessorAdapter is intended to be subclassed by OperationProcessor implementations that only modify the request or the response. Implementations the modify both should continue to implement OperationPreprocess directly. Closes gh-154 --- .../OperationPreprocessorAdapter.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java new file mode 100644 index 000000000..f2654c1bf --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * An implementation of {@link OperationPreprocessor} that returns the request and + * response as-is. To be subclasses by preprocessor implementations that only modify the + * request or the response. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public abstract class OperationPreprocessorAdapter implements OperationPreprocessor { + + /** + * Returns the given {@code request} as-is. + * + * @param request the request + * @return the unmodified request + */ + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + /** + * Returns the given {@code response} as-is. + * + * @param response the response + * @return the unmodified response + */ + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + +} From ecdc1971c29e05fc2afbdbb6cc2d8ab43793bf49 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 12:00:56 +0100 Subject: [PATCH 0248/1059] Add default descriptions for Hibernate Validator's constraints Closes gh-151 --- .../docs/asciidoc/documenting-your-api.adoc | 15 + .../DefaultConstraintDescriptions.properties | 14 +- ...dleConstraintDescriptionResolverTests.java | 263 ++++++++++++++---- 3 files changed, 233 insertions(+), 59 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 88313f978..3e409f6e5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -534,6 +534,21 @@ Default descriptions are provided for all of the Bean Validation 1.1's constrain * Pattern * Size +Default descriptions are also provided for constraints from Hibernate Validator: + +* CreditCardNumber +* EAN +* Email +* Length +* LuhnCheck +* Mod10Check +* Mod11Check +* NotBlank +* NotEmpty +* Range +* SafeHtml +* URL + To override the default descriptions, or to provide a new description, create a resource bundle with the base name `org.springframework.restdocs.constraints.ConstraintDescriptions`. The Spring diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index c945d4865..1cd5320f8 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -10,4 +10,16 @@ javax.validation.constraints.NotNull.description=Must not be null javax.validation.constraints.Null.description=Must be null javax.validation.constraints.Past.description=Must be in the past javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' -javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive \ No newline at end of file +javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive +org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number +org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number +org.hibernate.validator.constraints.Email.description=Must be a well-formed email address +org.hibernate.validator.constraints.Length.description=Length must be between ${min} and ${max} inclusive +org.hibernate.validator.constraints.LuhnCheck.description=Must pass the Luhn Modulo 10 checksum algorithm +org.hibernate.validator.constraints.Mod10Check.description=Must pass the Mod10 checksum algorithm +org.hibernate.validator.constraints.Mod11Check.description=Must pass the Mod11 checksum algorithm +org.hibernate.validator.constraints.NotBlank.description=Must not be blank +org.hibernate.validator.constraints.NotEmpty.description=Must not be empty +org.hibernate.validator.constraints.Range.description=Must be at least ${min} and at most ${max} +org.hibernate.validator.constraints.SafeHtml.description=Must be safe HTML +org.hibernate.validator.constraints.URL.description=Must be a well-formed URL \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 06334f51e..f9a7f734e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -16,11 +16,13 @@ package org.springframework.restdocs.constraints; +import java.lang.annotation.Annotation; +import java.math.BigDecimal; import java.net.URL; import java.util.Collections; -import java.util.HashMap; +import java.util.Date; +import java.util.List; import java.util.ListResourceBundle; -import java.util.Map; import java.util.ResourceBundle; import javax.validation.constraints.AssertFalse; @@ -37,8 +39,23 @@ import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.EAN; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.LuhnCheck; +import org.hibernate.validator.constraints.Mod10Check; +import org.hibernate.validator.constraints.Mod11Check; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; import org.junit.Test; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -54,111 +71,151 @@ public class ResourceBundleConstraintDescriptionResolverTests { @Test public void defaultMessageAssertFalse() { - String description = this.resolver.resolveDescription(new Constraint( - AssertFalse.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be false"))); + assertThat(constraintDescriptionForField("assertFalse"), + is(equalTo("Must be false"))); } @Test public void defaultMessageAssertTrue() { - String description = this.resolver.resolveDescription(new Constraint( - AssertTrue.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be true"))); + assertThat(constraintDescriptionForField("assertTrue"), + is(equalTo("Must be true"))); } @Test public void defaultMessageDecimalMax() { - Map configuration = new HashMap<>(); - configuration.put("value", "9.875"); - String description = this.resolver.resolveDescription( - new Constraint(DecimalMax.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at most 9.875"))); + assertThat(constraintDescriptionForField("decimalMax"), + is(equalTo("Must be at most 9.875"))); } @Test public void defaultMessageDecimalMin() { - Map configuration = new HashMap<>(); - configuration.put("value", "1.5"); - String description = this.resolver.resolveDescription( - new Constraint(DecimalMin.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at least 1.5"))); + assertThat(constraintDescriptionForField("decimalMin"), + is(equalTo("Must be at least 1.5"))); } @Test public void defaultMessageDigits() { - Map configuration = new HashMap<>(); - configuration.put("integer", "2"); - configuration.put("fraction", "5"); - String description = this.resolver.resolveDescription( - new Constraint(Digits.class.getName(), configuration)); - assertThat(description, is(equalTo( - "Must have at most 2 integral digits and 5 " + "fractional digits"))); + assertThat(constraintDescriptionForField("digits"), is( + equalTo("Must have at most 2 integral digits and 5 fractional digits"))); } @Test public void defaultMessageFuture() { - String description = this.resolver.resolveDescription(new Constraint( - Future.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be in the future"))); + assertThat(constraintDescriptionForField("future"), + is(equalTo("Must be in the future"))); } @Test public void defaultMessageMax() { - Map configuration = new HashMap<>(); - configuration.put("value", 10); - String description = this.resolver - .resolveDescription(new Constraint(Max.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at most 10"))); + assertThat(constraintDescriptionForField("max"), + is(equalTo("Must be at most 10"))); } @Test public void defaultMessageMin() { - Map configuration = new HashMap<>(); - configuration.put("value", 10); - String description = this.resolver - .resolveDescription(new Constraint(Min.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at least 10"))); + assertThat(constraintDescriptionForField("min"), + is(equalTo("Must be at least 10"))); } @Test public void defaultMessageNotNull() { - String description = this.resolver.resolveDescription(new Constraint( - NotNull.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must not be null"))); + assertThat(constraintDescriptionForField("notNull"), + is(equalTo("Must not be null"))); } @Test public void defaultMessageNull() { - String description = this.resolver.resolveDescription(new Constraint( - Null.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be null"))); + assertThat(constraintDescriptionForField("nul"), is(equalTo("Must be null"))); } @Test public void defaultMessagePast() { - String description = this.resolver.resolveDescription(new Constraint( - Past.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be in the past"))); + assertThat(constraintDescriptionForField("past"), + is(equalTo("Must be in the past"))); } @Test public void defaultMessagePattern() { - Map configuration = new HashMap<>(); - configuration.put("regexp", "[A-Z][a-z]+"); - String description = this.resolver.resolveDescription( - new Constraint(Pattern.class.getName(), configuration)); - assertThat(description, + assertThat(constraintDescriptionForField("pattern"), is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); } @Test public void defaultMessageSize() { - Map configuration = new HashMap<>(); - configuration.put("min", 2); - configuration.put("max", 10); - String description = this.resolver - .resolveDescription(new Constraint(Size.class.getName(), configuration)); - assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive"))); + assertThat(constraintDescriptionForField("size"), + is(equalTo("Size must be between 2 and 10 inclusive"))); + } + + @Test + public void defaultMessageCreditCardNumber() { + assertThat(constraintDescriptionForField("creditCardNumber"), + is(equalTo("Must be a well-formed credit card number"))); + } + + @Test + public void defaultMessageEan() { + assertThat(constraintDescriptionForField("ean"), + is(equalTo("Must be a well-formed EAN13 number"))); + } + + @Test + public void defaultMessageEmail() { + assertThat(constraintDescriptionForField("email"), + is(equalTo("Must be a well-formed email address"))); + } + + @Test + public void defaultMessageLength() { + assertThat(constraintDescriptionForField("length"), + is(equalTo("Length must be between 2 and 10 inclusive"))); + } + + @Test + public void defaultMessageLuhnCheck() { + assertThat(constraintDescriptionForField("luhnCheck"), + is(equalTo("Must pass the Luhn Modulo 10 checksum algorithm"))); + } + + @Test + public void defaultMessageMod10Check() { + assertThat(constraintDescriptionForField("mod10Check"), + is(equalTo("Must pass the Mod10 checksum algorithm"))); + } + + @Test + public void defaultMessageMod11Check() { + assertThat(constraintDescriptionForField("mod11Check"), + is(equalTo("Must pass the Mod11 checksum algorithm"))); + } + + @Test + public void defaultMessageNotBlank() { + assertThat(constraintDescriptionForField("notBlank"), + is(equalTo("Must not be blank"))); + } + + @Test + public void defaultMessageNotEmpty() { + assertThat(constraintDescriptionForField("notEmpty"), + is(equalTo("Must not be empty"))); + } + + @Test + public void defaultMessageRange() { + assertThat(constraintDescriptionForField("range"), + is(equalTo("Must be at least 10 and at most 100"))); + } + + @Test + public void defaultMessageSafeHtml() { + assertThat(constraintDescriptionForField("safeHtml"), + is(equalTo("Must be safe HTML"))); + } + + @Test + public void defaultMessageUrl() { + assertThat(constraintDescriptionForField("url"), + is(equalTo("Must be a well-formed URL"))); } @Test @@ -206,4 +263,94 @@ protected Object[][] getContents() { assertThat(description, is(equalTo("Not null"))); } + private String constraintDescriptionForField(String name) { + return this.resolver.resolveDescription(getConstraintFromField(name)); + } + + private Constraint getConstraintFromField(String name) { + Annotation[] annotations = ReflectionUtils.findField(Constrained.class, name) + .getAnnotations(); + Assert.isTrue(annotations.length == 1); + return new Constraint(annotations[0].annotationType().getName(), + AnnotationUtils.getAnnotationAttributes(annotations[0])); + } + + private static class Constrained { + + @AssertFalse + private boolean assertFalse; + + @AssertTrue + private boolean assertTrue; + + @DecimalMax("9.875") + private BigDecimal decimalMax; + + @DecimalMin("1.5") + private BigDecimal decimalMin; + + @Digits(integer = 2, fraction = 5) + private String digits; + + @Future + private Date future; + + @Max(10) + private int max; + + @Min(10) + private int min; + + @NotNull + private String notNull; + + @Null + private String nul; + + @Past + private Date past; + + @Pattern(regexp = "[A-Z][a-z]+") + private String pattern; + + @Size(min = 2, max = 10) + private List size; + + @CreditCardNumber + private String creditCardNumber; + + @EAN + private String ean; + + @Email + private String email; + + @Length(min = 2, max = 10) + private String length; + + @LuhnCheck + private String luhnCheck; + + @Mod10Check + private String mod10Check; + + @Mod11Check + private String mod11Check; + + @NotBlank + private String notBlank; + + @NotEmpty + private String notEmpty; + + @Range(min = 10, max = 100) + private int range; + + @SafeHtml + private String safeHtml; + + @org.hibernate.validator.constraints.URL + private String url; + } + } From ae53a4e8ebf6f5c1dd2d2715a7a66e5e96dcf41e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 12:36:50 +0100 Subject: [PATCH 0249/1059] Allow request and path parameters to be marked as optional Closes gh-169 --- .../docs/asciidoc/documenting-your-api.adoc | 5 ++-- .../request/AbstractParametersSnippet.java | 8 +++++- .../restdocs/request/ParameterDescriptor.java | 25 ++++++++++++++++++- .../request/PathParametersSnippetTests.java | 18 ++++++++++++- .../RequestParametersSnippetTests.java | 13 ++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 3e409f6e5..9550aaf50 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -348,7 +348,8 @@ table describing the parameters that are supported by the resource. When documenting request parameters, the test will fail if an undocumented request parameter is used in the request. Similarly, the test will also fail if a documented -request parameter is not found in the request. +request parameter is not found in the request and the request parameter has not been +marked as optional. If you do not want to document a request parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described @@ -396,7 +397,7 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than When documenting path parameters, the test will fail if an undocumented path parameter is used in the request. Similarly, the test will also fail if a documented path parameter -is not found in the request. +is not found in the request and the path parameter has not been marked as optional. If you do not want to document a path parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 21d900fe8..b132aa08d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -85,7 +85,13 @@ protected Map createModel(Operation operation) { private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); - Set expectedParameters = this.descriptorsByName.keySet(); + Set expectedParameters = new HashSet<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + if (!entry.getValue().isOptional()) { + expectedParameters.add(entry.getKey()); + } + } Set undocumentedParameters = new HashSet<>(actualParameters); undocumentedParameters.removeAll(expectedParameters); Set missingParameters = new HashSet<>(expectedParameters); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 9172a5525..d19eab4f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -29,6 +29,8 @@ public class ParameterDescriptor extends IgnorableDescriptor Date: Wed, 13 Apr 2016 14:09:08 +0100 Subject: [PATCH 0250/1059] Fix HalLinkExtractor's extraction of link hrefs Closes gh-220 --- .../restdocs/hypermedia/HalLinkExtractor.java | 17 ++++++++++------- .../hypermedia/LinkExtractorsPayloadTests.java | 4 ++-- .../link-payloads/atom/wrong-format.json | 8 ++++++-- .../hal/multiple-links-different-rels.json | 8 ++++++-- .../hal/multiple-links-same-rels.json | 6 +++++- .../link-payloads/hal/single-link.json | 4 +++- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index c3f28d733..eb55843aa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -54,9 +54,9 @@ private static List convertToLinks(Object object, String rel) { List links = new ArrayList<>(); if (object instanceof Collection) { @SuppressWarnings("unchecked") - Collection hrefObjects = (Collection) object; - for (Object hrefObject : hrefObjects) { - maybeAddLink(maybeCreateLink(rel, hrefObject), links); + Collection possibleLinkObjects = (Collection) object; + for (Object possibleLinkObject : possibleLinkObjects) { + maybeAddLink(maybeCreateLink(rel, possibleLinkObject), links); } } else { @@ -65,9 +65,12 @@ private static List convertToLinks(Object object, String rel) { return links; } - private static Link maybeCreateLink(String rel, Object possibleHref) { - if (possibleHref instanceof String) { - return new Link(rel, (String) possibleHref); + private static Link maybeCreateLink(String rel, Object possibleLinkObject) { + if (possibleLinkObject instanceof Map) { + Object hrefObject = ((Map) possibleLinkObject).get("href"); + if (hrefObject instanceof String) { + return new Link(rel, (String) hrefObject); + } } return null; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 7353bbeae..b26374e81 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -53,7 +53,7 @@ public class LinkExtractorsPayloadTests { private final String linkType; - @Parameters + @Parameters(name = "{1}") public static Collection data() { return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" }, new Object[] { new AtomLinkExtractor(), "atom" }); diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json index 04a6d84b8..a5e2e40fd 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json @@ -1,5 +1,9 @@ { - "_links": { - "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + "links": { + "alpha": [{ + "href": "http://alpha.example.com/one" + }, { + "href": "http://alpha.example.com/two" + }] } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json index 80d36d72a..08d0f3eaf 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -1,6 +1,10 @@ { "_links": { - "alpha": "http://alpha.example.com", - "bravo": "http://bravo.example.com" + "alpha": { + "href": "http://alpha.example.com" + }, + "bravo": { + "href": "http://bravo.example.com" + } } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json index 04a6d84b8..45ebca3ca 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -1,5 +1,9 @@ { "_links": { - "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + "alpha": [{ + "href": "http://alpha.example.com/one" + }, { + "href": "http://alpha.example.com/two" + }] } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json index be90b3779..d19ca125c 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -1,5 +1,7 @@ { "_links": { - "alpha": "http://alpha.example.com" + "alpha": { + "href": "http://alpha.example.com" + } } } \ No newline at end of file From c4b74387080bd71e1aaf6acba4bf150ffd3e2dd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 14:01:33 +0100 Subject: [PATCH 0251/1059] Derive a link's description from its title Previously, a LinkDescriptor had to be created with both a rel and a description. If a description was not provided a failure would occur. This commit relaxes the above-described restriction by allowing a link's title to be used as its default description. If a descriptor has a description, it will always be used irrespective of whether or not the link has a title. If the descriptor does not have a description and the link does have a title, the link's title will be used. If the descriptor does not have a description and the link does not have a title a failure will occur. Closes gh-105 --- .../docs/asciidoc/documenting-your-api.adoc | 4 ++ .../hypermedia/AtomLinkExtractor.java | 4 +- .../restdocs/hypermedia/HalLinkExtractor.java | 7 ++- .../hypermedia/HypermediaDocumentation.java | 4 ++ .../restdocs/hypermedia/Link.java | 36 ++++++++++++++- .../restdocs/hypermedia/LinksSnippet.java | 45 +++++++++++++++---- .../LinkExtractorsPayloadTests.java | 8 ++-- .../hypermedia/LinksSnippetFailureTests.java | 12 +++++ .../hypermedia/LinksSnippetTests.java | 14 ++++++ .../atom/multiple-links-different-rels.json | 3 +- .../atom/multiple-links-same-rels.json | 3 +- .../link-payloads/atom/single-link.json | 3 +- .../hal/multiple-links-different-rels.json | 3 +- .../hal/multiple-links-same-rels.json | 3 +- .../link-payloads/hal/single-link.json | 3 +- ...kMvcRestDocumentationIntegrationTests.java | 6 ++- ...uredRestDocumentationIntegrationTests.java | 6 ++- 17 files changed, 137 insertions(+), 27 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9550aaf50..2dee3919d 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -38,6 +38,10 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] The result is a snippet named `links.adoc` that contains a table describing the resource's links. +TIP: If a link in the response has a `title`, the description can be omitted from its +descriptor and the `title` will be used. If you omit the description and the link does +not have a `title` a failure will occur. + When documenting links, the test will fail if an undocumented link is found in the response. Similarly, the test will also fail if a documented link is not found in the response and the link has not been marked as optional. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java index ad12497a3..71a2d8dc7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -51,7 +51,9 @@ private static Link maybeCreateLink(Map linkMap) { Object hrefObject = linkMap.get("href"); Object relObject = linkMap.get("rel"); if (relObject instanceof String && hrefObject instanceof String) { - return new Link((String) relObject, (String) hrefObject); + Object titleObject = linkMap.get("title"); + return new Link((String) relObject, (String) hrefObject, + titleObject instanceof String ? (String) titleObject : null); } return null; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index eb55843aa..866a8322f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -67,9 +67,12 @@ private static List convertToLinks(Object object, String rel) { private static Link maybeCreateLink(String rel, Object possibleLinkObject) { if (possibleLinkObject instanceof Map) { - Object hrefObject = ((Map) possibleLinkObject).get("href"); + Map possibleLinkMap = (Map) possibleLinkObject; + Object hrefObject = possibleLinkMap.get("href"); if (hrefObject instanceof String) { - return new Link(rel, (String) hrefObject); + Object titleObject = possibleLinkMap.get("title"); + return new Link(rel, (String) hrefObject, + titleObject instanceof String ? (String) titleObject : null); } } return null; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index c2f11b75d..f54931283 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -53,6 +53,10 @@ public static LinkDescriptor linkWithRel(String rel) { * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 112886f0b..01d5165ce 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -29,6 +29,8 @@ public class Link { private final String href; + private final String title; + /** * Creates a new {@code Link} with the given {@code rel} and {@code href}. * @@ -36,8 +38,21 @@ public class Link { * @param href The link's href */ public Link(String rel, String href) { + this(rel, href, null); + } + + /** + * Creates a new {@code Link} with the given {@code rel}, {@code href}, and + * {@code title}. + * + * @param rel The link's rel + * @param href The link's href + * @param title The link's title + */ + public Link(String rel, String href, String title) { this.rel = rel; this.href = href; + this.title = title; } /** @@ -56,12 +71,21 @@ public String getHref() { return this.href; } + /** + * Returns the link's {@code title}, or {@code null} if it does not have a title. + * @return the link's {@code title} or {@code null} + */ + public String getTitle() { + return this.title; + } + @Override public int hashCode() { - int prime = 31; + final int prime = 31; int result = 1; result = prime * result + this.href.hashCode(); result = prime * result + this.rel.hashCode(); + result = prime * result + ((this.title == null) ? 0 : this.title.hashCode()); return result; } @@ -83,13 +107,21 @@ public boolean equals(Object obj) { if (!this.rel.equals(other.rel)) { return false; } + if (this.title == null) { + if (other.title != null) { + return false; + } + } + else if (!this.title.equals(other.title)) { + return false; + } return true; } @Override public String toString() { return new ToStringCreator(this).append("rel", this.rel).append("href", this.href) - .toString(); + .append("title", this.title).toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 7c684ba9f..528b444a5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -77,12 +77,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); - if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for link '" + descriptor.getRel() - + "' must either have a description or be" + " marked as " - + "ignored"); - } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } } @@ -90,14 +84,16 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip @Override protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); + Map> links; try { - validate(this.linkExtractor.extractLinks(response)); + links = this.linkExtractor.extractLinks(response); + validate(links); } catch (IOException ex) { throw new ModelCreationException(ex); } Map model = new HashMap<>(); - model.put("links", createLinksModel()); + model.put("links", createLinksModel(links)); return model; } @@ -135,17 +131,48 @@ private void validate(Map> links) { } } - private List> createLinksModel() { + private List> createLinksModel(Map> links) { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { LinkDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { + if (descriptor.getDescription() == null) { + descriptor = createDescriptor( + getDescriptionFromLinkTitle(links, descriptor.getRel()), + descriptor); + } model.add(createModelForDescriptor(descriptor)); } } return model; } + private String getDescriptionFromLinkTitle(Map> links, + String rel) { + List linksForRel = links.get(rel); + if (linksForRel != null) { + for (Link link : linksForRel) { + if (link.getTitle() != null) { + return link.getTitle(); + } + } + } + throw new SnippetException("No description was provided for the link with rel '" + + rel + "' and no title was available from the link in the payload"); + } + + private LinkDescriptor createDescriptor(String description, LinkDescriptor source) { + LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel()) + .description(description); + if (source.isOptional()) { + newDescriptor.optional(); + } + if (source.isIgnored()) { + newDescriptor.ignored(); + } + return newDescriptor; + } + /** * Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their * {@link LinkDescriptor#getRel() rels}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index b26374e81..fa5781645 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -68,14 +68,15 @@ public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) public void singleLink() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("single-link")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha")), + links); } @Test public void multipleLinksWithDifferentRels() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("multiple-links-different-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha"), new Link("bravo", "http://bravo.example.com")), links); } @@ -83,7 +84,8 @@ public void multipleLinksWithDifferentRels() throws IOException { public void multipleLinksWithSameRels() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("multiple-links-same-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), + assertLinks(Arrays.asList( + new Link("alpha", "http://alpha.example.com/one", "Alpha one"), new Link("alpha", "http://alpha.example.com/two")), links); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 6ef2dfd8a..217b753e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -79,4 +79,16 @@ public void undocumentedLinkAndMissingLink() throws IOException { this.snippet.getOutputDirectory()).build()); } + @Test + public void linkWithNoDescription() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("No description was provided for the link with rel 'foo' and no" + + " title was available from the link in the payload")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Arrays.asList(new LinkDescriptor("foo"))) + .document(new OperationBuilder("link-with-no-description", + this.snippet.getOutputDirectory()).build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b78b3b678..be122de6f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -87,6 +87,20 @@ public void documentedLinks() throws IOException { .document(operationBuilder("documented-links").build()); } + @Test + public void linkDescriptionFromTitleInPayload() throws IOException { + this.snippet.expectLinks("link-description-from-title-in-payload") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") + .row("b", "Link b")); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), + new Link("b", "bravo", "Link b")), + Arrays.asList(new LinkDescriptor("a").description("one"), + new LinkDescriptor("b"))).document( + operationBuilder("link-description-from-title-in-payload") + .build()); + } + @Test public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json index 35e63537d..081c5d98d 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json @@ -1,7 +1,8 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" }, { "rel": "bravo", "href": "http://bravo.example.com" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json index ff0d3f4ae..28d76ac7d 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json @@ -1,7 +1,8 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com/one" + "href": "http://alpha.example.com/one", + "title": "Alpha one" }, { "rel": "alpha", "href": "http://alpha.example.com/two" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json index 57532675b..d6339c031 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json @@ -1,6 +1,7 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } ] } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json index 08d0f3eaf..6eecbf5c6 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" }, "bravo": { "href": "http://bravo.example.com" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json index 45ebca3ca..9e5f51dcd 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -1,7 +1,8 @@ { "_links": { "alpha": [{ - "href": "http://alpha.example.com/one" + "href": "http://alpha.example.com/one", + "title": "Alpha one" }, { "href": "http://alpha.example.com/two" }] diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json index d19ca125c..43bf4273c 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } } } \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 51c35ee4d..54bce09ff 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -38,7 +38,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -461,7 +460,10 @@ private static class TestController { public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + response.put("links", Arrays.asList(link)); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); return new ResponseEntity<>(response, headers, HttpStatus.OK); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 8d4da39a6..408d29059 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -40,7 +40,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -329,7 +328,10 @@ static class TestApplication { public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + response.put("links", Arrays.asList(link)); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); headers.add("Foo", "http://localhost:12345/foo/bar"); From 2897b8d1b4d2d6923b6376c37dc58b94a7f7d3f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 17:50:19 +0100 Subject: [PATCH 0252/1059] =?UTF-8?q?Provide=20a=20preprocessor=20for=20mo?= =?UTF-8?q?difying=20a=20request=E2=80=99s=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes gh-155 --- .../customizing-requests-and-responses.adoc | 9 + .../operation/OperationRequestFactory.java | 20 +- ...ametersModifyingOperationPreprocessor.java | 181 ++++++++++++++++++ .../operation/preprocess/Preprocessors.java | 13 +- ...rsModifyingOperationPreprocessorTests.java | 136 +++++++++++++ 5 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 8cb020544..04a1b48e1 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -109,6 +109,7 @@ the name is equal to any of the given header names. response where the name matches any of the given regular expression patterns. + [[customizing-requests-and-responses-preprocessors-replace-patterns]] ==== Replacing patterns @@ -118,6 +119,14 @@ replaced. +[[customizing-requests-and-responses-preprocessors-modify-request-parameters]] +==== Modifying request parameters + +`modifyParameters` on `Preprocessors` can be used to add, set, and remove request +parameters. + + + [[customizing-requests-and-responses-preprocessors-modify-uris]] ==== Modifying URIs diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index c105fd75c..961987b5b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -72,7 +72,7 @@ public OperationRequest createFrom(OperationRequest original, byte[] newContent) * @param original The original request * @param newHeaders The new headers * - * @return The new request with the new content + * @return The new request with the new headers */ public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { @@ -81,6 +81,22 @@ public OperationRequest createFrom(OperationRequest original, original.getParts()); } + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newParameters}. + * + * @param original The original request + * @param newParameters The new parameters + * + * @return The new request with the new parameters + */ + public OperationRequest createFrom(OperationRequest original, + Parameters newParameters) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + original.getContent(), original.getHeaders(), newParameters, + original.getParts()); + } + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { return new HttpHeadersHelper(originalHeaders) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java new file mode 100644 index 000000000..5006c73e3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java @@ -0,0 +1,181 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.util.Assert; + +/** + * An {@link OperationPreprocessor} that can be used to modify a request's + * {@link OperationRequest#getParameters()} by adding, setting, and removing parameters. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public final class ParametersModifyingOperationPreprocessor + extends OperationPreprocessorAdapter { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final List modifications = new ArrayList<>(); + + @Override + public OperationRequest preprocess(OperationRequest request) { + Parameters parameters = new Parameters(); + parameters.putAll(request.getParameters()); + for (Modification modification : this.modifications) { + modification.apply(parameters); + } + return this.requestFactory.createFrom(request, parameters); + } + + /** + * Adds a parameter with the given {@code name} and {@code value}. + * + * @param name the name + * @param value the value + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor add(String name, String value) { + this.modifications.add(new AddParameterModification(name, value)); + return this; + } + + /** + * Sets the parameter with the given {@code name} to have the given {@code values}. + * + * @param name the name + * @param values the values + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor set(String name, String... values) { + Assert.notEmpty(values, "At least one value must be provided"); + this.modifications.add(new SetParameterModification(name, Arrays.asList(values))); + return this; + } + + /** + * Removes the parameter with the given {@code name}. + * + * @param name the name of the parameter + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor remove(String name) { + this.modifications.add(new RemoveParameterModification(name)); + return this; + } + + /** + * Removes the given {@code value} from the parameter with the given {@code name}. + * + * @param name the name + * @param value the value + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor remove(String name, String value) { + this.modifications.add(new RemoveValueParameterModification(name, value)); + return this; + } + + private interface Modification { + + void apply(Parameters parameters); + + } + + private static final class AddParameterModification implements Modification { + + private final String name; + + private final String value; + + private AddParameterModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void apply(Parameters parameters) { + parameters.add(this.name, this.value); + } + + } + + private static final class SetParameterModification implements Modification { + + private final String name; + + private final List values; + + private SetParameterModification(String name, List values) { + this.name = name; + this.values = values; + } + + @Override + public void apply(Parameters parameters) { + parameters.put(this.name, this.values); + } + + } + + private static final class RemoveParameterModification implements Modification { + + private final String name; + + private RemoveParameterModification(String name) { + this.name = name; + } + + @Override + public void apply(Parameters parameters) { + parameters.remove(this.name); + } + + } + + private static final class RemoveValueParameterModification implements Modification { + + private final String name; + + private final String value; + + private RemoveValueParameterModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void apply(Parameters parameters) { + List values = parameters.get(this.name); + if (values != null) { + values.remove(this.value); + if (values.isEmpty()) { + parameters.remove(this.name); + } + } + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index d600407aa..23a310378 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -127,7 +127,7 @@ public static OperationPreprocessor maskLinks(String mask) { /** * Returns an {@code OperationPreprocessor} that will modify the content of the - * request or response by replacing occurences of the given {@code pattern} with the + * request or response by replacing occurrences of the given {@code pattern} with the * given {@code replacement}. * * @param pattern the pattern @@ -140,4 +140,15 @@ public static OperationPreprocessor replacePattern(Pattern pattern, new PatternReplacingContentModifier(pattern, replacement)); } + /** + * Returns a {@code ParametersModifyingOperationPreprocessor} that can then be + * configured to modify the parameters of the request. + * + * @return the preprocessor + * @since 1.1.0 + */ + public static ParametersModifyingOperationPreprocessor modifyParameters() { + return new ParametersModifyingOperationPreprocessor(); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..4d8035f39 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import java.net.URI; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ParametersModifyingOperationPreprocessor}. + * + * @author Andy Wilkinson + */ +public class ParametersModifyingOperationPreprocessorTests { + + private final ParametersModifyingOperationPreprocessor preprocessor = new ParametersModifyingOperationPreprocessor(); + + @Test + public void addNewParameter() { + Parameters parameters = new Parameters(); + assertThat(this.preprocessor.add("a", "alpha") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha"))); + } + + @Test + public void addValueToExistingParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.add("a", "alpha").preprocess(createRequest(parameters)) + .getParameters(), + hasEntry(equalTo("a"), contains("apple", "alpha"))); + } + + @Test + public void setNewParameter() { + Parameters parameters = new Parameters(); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void setExistingParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void removeNonExistentParameter() { + Parameters parameters = new Parameters(); + assertThat(this.preprocessor.remove("a").preprocess(createRequest(parameters)) + .getParameters().size(), is(equalTo(0))); + } + + @Test + public void removeParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void removeParameterValueForNonExistentParameter() { + Parameters parameters = new Parameters(); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters().size(), + is(equalTo(0))); + } + + @Test + public void removeParameterValueWithMultipleValues() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + parameters.add("a", "alpha"); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha"))); + } + + @Test + public void removeParameterValueWithSingleValueRemovesEntryEntirely() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters().size(), + is(equalTo(0))); + } + + private OperationRequest createRequest(Parameters parameters) { + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), + HttpMethod.GET, new byte[0], new HttpHeaders(), parameters, + Collections.emptyList()); + } + +} From b62c5f0374e109fe0b9fee8a63ca34572127f006 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 10:30:52 +0100 Subject: [PATCH 0253/1059] Make it easier to document common portions of req and resp payloads This commit adds a new andWithPrefix(String, FieldDescriptor[]) method to both RequestFieldsSnippet and ResponseFieldsSnippet. It can be used to add descriptors to an existing snippet, applying the given prefix to the additional descriptors as it does so. This allows the descriptors for a portion of a payload to be created once and then reused, irrespective of where in the payload the portion appears. Closes gh-221 --- .../docs/asciidoc/documenting-your-api.adoc | 79 +++++++++++++++++++ docs/src/test/java/com/example/Payload.java | 34 ++++++++ .../java/com/example/mockmvc/Payload.java | 21 +++++ .../java/com/example/restassured/Payload.java | 24 ++++++ .../payload/AbstractFieldsSnippet.java | 26 ++++++ .../payload/RequestFieldsSnippet.java | 19 ++++- .../payload/ResponseFieldsSnippet.java | 19 ++++- .../payload/RequestFieldsSnippetTests.java | 15 ++++ .../payload/ResponseFieldsSnippetTests.java | 14 ++++ 9 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 docs/src/test/java/com/example/Payload.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 2dee3919d..f798b34a5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -296,6 +296,85 @@ method will be used in the documentation. +[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] +==== Reusing field descriptors + +In addition to the general support for <>, the request and response snippets allow additional descriptors to be +configured with a path prefix. This allows the descriptors for a repeated portion of a +request or response payload to be created once and then reused. + +Consider an endpoint that returns a book: + +[source,json,indent=0] +---- + { + "title": "Pride and Prejudice", + "author": "Jane Austen" + } +---- + +The paths for `title` and `author` are simply `title` and `author` respectively. + +Now consider an endpoint that returns an array of books: + +[source,json,indent=0] +---- + [{ + "title": "Pride and Prejudice", + "author": "Jane Austen" + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee" + }] +---- + +The paths for `title` and `author` are `[].title` and `[].author` respectively. The only +difference between the single book and the array of books is that the fields' paths now +have a `[].` prefix. + +The descriptors that document a book can be created: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=book-descriptors] +---- + +They can then be used to document a single book: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +And an array of books: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java new file mode 100644 index 000000000..d33de4d16 --- /dev/null +++ b/docs/src/test/java/com/example/Payload.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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 com.example; + +import org.springframework.restdocs.payload.FieldDescriptor; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class Payload { + + @SuppressWarnings("unused") + public void bookFieldDescriptors() { + // tag::book-descriptors[] + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + // end::book-descriptors[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 4424b1f31..b811c1a6c 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; @@ -70,4 +71,24 @@ public void constraints() throws Exception { // end::constraints[] } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields(book))); // <1> + // end::single-book[] + + // tag::book-array[] + this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> + // end::book-array[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index ee6c2456f..225eb6194 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -16,6 +16,7 @@ package com.example.restassured; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import com.jayway.restassured.RestAssured; @@ -72,4 +73,27 @@ public void constraints() throws Exception { .then().assertThat().statusCode(is(200)); } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("book", responseFields(book))) // <1> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::single-book[] + + // tag::book-array[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("books", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))) // <2> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::book-array[] + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index dcff13bdd..19322b281 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -180,4 +180,30 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + protected final List applyPathPrefix(String pathPrefix, + List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + FieldDescriptor prefixedDescriptor = new FieldDescriptor( + pathPrefix + descriptor.getPath()) + .description(descriptor.getDescription()) + .type(descriptor.getType()); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index fdc3e609e..9e2968873 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -72,13 +72,30 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code RequestFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 14d5dd555..28815c4ce 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -72,13 +72,30 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public ResponseFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 195ed58a8..79e02a8c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -176,4 +176,19 @@ public void additionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectRequestFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors") + .request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8e33fadad..ae490e50d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -232,4 +232,18 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectResponseFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors").response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } From 0e52ef04a1a378e03a3b27cb4163fcba8de933ba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 11:18:27 +0100 Subject: [PATCH 0254/1059] Document how to ignore common links without repetition Closes gh-177 --- .../docs/asciidoc/documenting-your-api.adoc | 16 +++++++++ .../src/test/java/com/example/Hypermedia.java | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/src/test/java/com/example/Hypermedia.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index f798b34a5..9242fcbc3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -84,6 +84,22 @@ If your API represents its links in a format other than Atom or HAL, you can pro own implementation of the `LinkExtractor` interface to extract the links from the response. + + +[[documenting-your-api-hypermedia-ignoring-common-links]] +==== Ignoring common links + +Rather than documenting links that are common to every response, such as `_self` and +`curies` when using HAL, you may want to document them once in an overview section and +then ignore them in the rest of your API's documentation. To do so, you can build on the +<> to add link +descriptors to a snippet that's preconfigured to ignore certain links. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] +---- + [[documenting-your-api-request-response-payloads]] === Request and response payloads diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java new file mode 100644 index 000000000..10f168684 --- /dev/null +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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 com.example; + +import org.springframework.restdocs.hypermedia.HypermediaDocumentation; +import org.springframework.restdocs.hypermedia.LinkDescriptor; +import org.springframework.restdocs.hypermedia.LinksSnippet; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; + +public class Hypermedia { + + // tag::ignore-links[] + public static LinksSnippet links(LinkDescriptor... descriptors) { + return HypermediaDocumentation.links(linkWithRel("_self").ignored().optional(), + linkWithRel("curies").ignored()).and(descriptors); + } + // end::ignore-links[] + +} From 043e796502d7b283da15b6da46305976c7e671dc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 12:41:05 +0100 Subject: [PATCH 0255/1059] =?UTF-8?q?Introduce=20relaxed=20snippets=20that?= =?UTF-8?q?=20don=E2=80=99t=20fail=20when=20item=20is=20not=20documented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, many of the snippets would fail when something wasn’t documented. This worked well when writing exhaustive API documentation, but was cumbersome when trying to document a scenario that might only being interested in a subset of the links, response fields, etc. It was necessary to mark things that were not of interest as being ignored. This commit introduces a relaxed variant of several snippets. A relaxed snippet will not fail if something has not been documented. Instead, the undocumented thing will be ignored. If something has been documented but it does not exist a failure will still occur. Closes gh-175 --- .../hypermedia/HypermediaDocumentation.java | 100 ++++++++++++++++++ .../restdocs/hypermedia/LinksSnippet.java | 50 ++++++++- .../payload/AbstractFieldsSnippet.java | 32 +++++- .../payload/PayloadDocumentation.java | 69 ++++++++++++ .../payload/RequestFieldsSnippet.java | 39 ++++++- .../payload/ResponseFieldsSnippet.java | 40 ++++++- .../request/AbstractParametersSnippet.java | 38 ++++++- .../request/PathParametersSnippet.java | 42 +++++++- .../request/RequestDocumentation.java | 72 ++++++++++++- .../request/RequestParametersSnippet.java | 45 +++++++- .../hypermedia/LinksSnippetTests.java | 11 ++ .../payload/RequestFieldsSnippetTests.java | 13 +++ .../payload/ResponseFieldsSnippetTests.java | 12 +++ .../request/PathParametersSnippetTests.java | 13 +++ .../RequestParametersSnippetTests.java | 11 ++ 15 files changed, 559 insertions(+), 28 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index f54931283..95f70b3be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -66,6 +66,26 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -80,6 +100,10 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param descriptors the descriptions of the response's links @@ -91,6 +115,29 @@ public static LinksSnippet links(Map attributes, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API call's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response automatically based on its content type + * and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(Map attributes, + LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), attributes, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -104,6 +151,10 @@ public static LinksSnippet links(Map attributes, * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links @@ -114,6 +165,27 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -128,6 +200,10 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param linkExtractor used to extract the links from the response @@ -139,6 +215,30 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + Map attributes, LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes, + true); + } + /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 528b444a5..51b28f927 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -50,22 +51,41 @@ public class LinksSnippet extends TemplatedSnippet { private final LinkExtractor linkExtractor; + private final boolean ignoreUndocumentedLinks; + /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { - this(linkExtractor, descriptors, null); + this(linkExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. If + * {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be ignored + * and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + boolean ignoreUndocumentedLinks) { + this(linkExtractor, descriptors, null, ignoreUndocumentedLinks); } /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. The * given {@code attributes} will be included in the model during template rendering. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors @@ -73,12 +93,30 @@ protected LinksSnippet(LinkExtractor linkExtractor, */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, Map attributes) { + this(linkExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. The + * given {@code attributes} will be included in the model during template rendering. + * If {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be + * ignored and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + Map attributes, boolean ignoreUndocumentedLinks) { super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); this.descriptorsByRel.put(descriptor.getRel(), descriptor); } + this.ignoreUndocumentedLinks = ignoreUndocumentedLinks; } @Override @@ -100,8 +138,14 @@ protected Map createModel(Operation operation) { private void validate(Map> links) { Set actualRels = links.keySet(); - Set undocumentedRels = new HashSet<>(actualRels); - undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + Set undocumentedRels; + if (this.ignoreUndocumentedLinks) { + undocumentedRels = Collections.emptySet(); + } + else { + undocumentedRels = new HashSet<>(actualRels); + undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + } Set requiredRels = new HashSet<>(); for (Entry relAndDescriptor : this.descriptorsByRel diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 19322b281..c64507128 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -39,20 +39,42 @@ */ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { - private List fieldDescriptors; + private final List fieldDescriptors; + + private final boolean ignoreUndocumentedFields; /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented fields will trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes + * @deprecated since 1.1 in favor of + * {@link #AbstractFieldsSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractFieldsSnippet(String type, List descriptors, Map attributes) { + this(type, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); @@ -65,6 +87,7 @@ protected AbstractFieldsSnippet(String type, List descriptors, } this.fieldDescriptors = descriptors; + this.ignoreUndocumentedFields = ignoreUndocumentedFields; } @Override @@ -111,8 +134,9 @@ private ContentHandler getContentHandler(Operation operation) { private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); - String undocumentedPayload = payloadHandler - .getUndocumentedContent(this.fieldDescriptors); + + String undocumentedPayload = this.ignoreUndocumentedFields ? null + : payloadHandler.getUndocumentedContent(this.fieldDescriptors); if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { String message = ""; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index f2391277b..bb94c960d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -119,6 +119,22 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) return new RequestFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -145,6 +161,24 @@ public static RequestFieldsSnippet requestFields(Map attributes, return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + Map attributes, FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -169,6 +203,23 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -195,4 +246,22 @@ public static ResponseFieldsSnippet responseFields(Map attribute return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + Map attributes, FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 9e2968873..cfb2ee265 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -37,25 +37,56 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the - * request using the given {@code descriptors}. + * request using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected RequestFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected RequestFieldsSnippet(List descriptors, Map attributes) { - super("request", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 28815c4ce..06b5ac655 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -37,25 +37,57 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the - * response using the given {@code descriptors}. + * response using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected ResponseFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. If {@code ignoreUndocumentedFields} + * is {@code true}, undocumented fields will be ignored and will not trigger a + * failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected ResponseFieldsSnippet(List descriptors, Map attributes) { - super("response", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index b132aa08d..9d97737d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.request; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,18 +41,42 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); + private final boolean ignoreUndocumentedParameters; + /** * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with * the given {@code snippetName} that will document parameters using the given * {@code descriptors}. The given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented parameters will trigger a failure. * * @param snippetName The snippet name * @param descriptors The descriptors * @param attributes The additional attributes + * @deprecated since 1.1 in favour of + * {@link #AbstractParametersSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { + this(snippetName, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with + * the given {@code snippetName} that will document parameters using the given + * {@code descriptors}. The given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedParameters} is {@code true}, + * undocumented parameters will be ignored and will not trigger a failure. + * + * @param snippetName The snippet name + * @param descriptors The descriptors + * @param attributes The additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected AbstractParametersSnippet(String snippetName, + List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getName(), @@ -64,6 +89,7 @@ protected AbstractParametersSnippet(String snippetName, } this.descriptorsByName.put(descriptor.getName(), descriptor); } + this.ignoreUndocumentedParameters = ignoreUndocumentedParameters; } @Override @@ -92,8 +118,14 @@ private void verifyParameterDescriptors(Operation operation) { expectedParameters.add(entry.getKey()); } } - Set undocumentedParameters = new HashSet<>(actualParameters); - undocumentedParameters.removeAll(expectedParameters); + Set undocumentedParameters; + if (this.ignoreUndocumentedParameters) { + undocumentedParameters = Collections.emptySet(); + } + else { + undocumentedParameters = new HashSet<>(actualParameters); + undocumentedParameters.removeAll(expectedParameters); + } Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 01ef25e41..509a604b7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -44,25 +44,59 @@ public class PathParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code PathParametersSnippet} that will document the request's path - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected PathParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code PathParametersSnippet} that will document the request's path * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected PathParametersSnippet(List descriptors, Map attributes) { - super("path-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("path-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 6fa64c9bf..4c4aee73d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -65,6 +65,22 @@ public static PathParametersSnippet pathParameters( return new PathParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -89,6 +105,24 @@ public static PathParametersSnippet pathParameters(Map attribute return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -112,6 +146,23 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -137,4 +188,23 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index e85423fc1..37a5696d6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -42,25 +42,60 @@ public class RequestParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code RequestParametersSnippet} that will document the request's - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected RequestParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code RequestParametersSnippet} that will document the request's * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected RequestParametersSnippet(List descriptors, Map attributes) { - super("request-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("request-parameters", descriptors, attributes, + ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index be122de6f..e94abfc35 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -56,6 +56,17 @@ public void ignoredLink() throws IOException { .document(operationBuilder("ignored-link").build()); } + @Test + public void allUndocumentedLinksCanBeIgnored() throws IOException { + this.snippet.expectLinks("ignore-all-undocumented").withContents( + tableWithHeader("Relation", "Description").row("b", "Link b")); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("b").description("Link b")), true) + .document(operationBuilder("ignore-all-undocumented").build()); + } + @Test public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 79e02a8c6..d94a9071c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -93,6 +93,19 @@ public void ignoredRequestField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { + this.snippet.expectRequestFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), + true).document( + operationBuilder("ignore-all-undocumented") + .request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ae490e50d..215eb4c87 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -105,6 +105,18 @@ public void ignoredResponseField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedFieldsCanBeIgnored() throws IOException { + this.snippet.expectResponseFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(operationBuilder("ignore-all-undocumented").response() + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 05ca00c91..c34430481 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -71,6 +71,19 @@ public void ignoredPathParameter() throws IOException { "/{a}/{b}").build()); } + @Test + public void allUndocumentedPathParametersCanBeIgnored() throws IOException { + this.snippet.expectPathParameters("ignore-all-undocumented").withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + "two")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), + true).document(operationBuilder("ignore-all-undocumented") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); + } + @Test public void missingOptionalPathParameter() throws IOException { this.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 16baa187c..a62c5de2c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -80,6 +80,17 @@ public void ignoredRequestParameter() throws IOException { .param("b", "bravo").build()); } + @Test + public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { + this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( + tableWithHeader("Parameter", "Description").row("b", "two")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), true) + .document(operationBuilder("ignore-all-undocumented") + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); + } + @Test public void missingOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("missing-optional-request-parameter") From a3e688352441172be325ee69b3466f0722ffdef0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 09:50:01 +0100 Subject: [PATCH 0256/1059] Apply Eclipse Mars (4.5.2) code formatting --- .../curl/CurlRequestSnippetTests.java | 19 ++++---- .../headers/RequestHeadersSnippetTests.java | 43 ++++++++--------- .../headers/ResponseHeadersSnippetTests.java | 29 ++++++----- .../http/HttpRequestSnippetTests.java | 19 ++++---- .../PrettyPrintingContentModifierTests.java | 2 +- .../payload/RequestFieldsSnippetTests.java | 43 +++++++++-------- .../payload/ResponseFieldsSnippetTests.java | 48 ++++++++++--------- .../request/PathParametersSnippetTests.java | 38 ++++++++------- .../RequestParametersSnippetTests.java | 21 ++++---- ...kMvcRestDocumentationIntegrationTests.java | 5 +- 10 files changed, 145 insertions(+), 122 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 519644337..5e5b44c76 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -262,8 +262,8 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); } @Test @@ -279,9 +279,11 @@ public void multipartPostWithContentType() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png") + .build()); } @Test @@ -314,9 +316,10 @@ public void multipartPostWithParameters() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 279371693..5f493033c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -62,20 +62,18 @@ public void requestWithHeaders() throws IOException { .row("Accept", "two").row("Accept-Encoding", "three") .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"), - headerWithName("Accept").description("two"), - headerWithName("Accept-Encoding").description("three"), - headerWithName("Accept-Language").description("four"), - headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test") - .header("Accept", "*/*") - .header("Accept-Encoding", - "gzip, deflate") + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four"), + headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six"))) + .document(new OperationBuilder("request-with-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") .header("Accept-Language", "en-US,en;q=0.5") .header("Cache-Control", "max-age=0") .header("Connection", "keep-alive").build()); @@ -143,14 +141,15 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-extra-column")); - new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one") - .attributes(key("foo").value("alpha")), - headerWithName("Accept-Encoding").description("two") - .attributes(key("foo").value("bravo")), - headerWithName("Accept").description("three") - .attributes(key("foo").value("charlie")))) - .document(new OperationBuilder( + new RequestHeadersSnippet( + Arrays.asList( + headerWithName("X-Test").description("one") + .attributes(key("foo").value( + "alpha")), + headerWithName("Accept-Encoding").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes(key( + "foo").value("charlie")))).document(new OperationBuilder( "request-headers-with-custom-attributes", this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 8f5dd609f..41fe5fcd2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -72,9 +72,9 @@ public void responseWithHeaders() throws IOException { .header("X-Test", "test") .header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0") - .header("Vary", "User-Agent").build()); + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); } @Test @@ -107,15 +107,20 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response().header("X-Test", "test") - .header("Content-Type", - "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", + "lskjadldj3ii32l2ij23") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 5175edd8c..ed0302323 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -92,8 +92,8 @@ public void postRequestWithContent() throws IOException { String content = "Hello, world"; this.snippet.expectHttpRequest("post-request-with-content") .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(new OperationBuilder("post-request-with-content", @@ -154,8 +154,8 @@ public void putRequestWithContent() throws IOException { String content = "Hello, world"; this.snippet.expectHttpRequest("put-request-with-content") .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder("put-request-with-content", this.snippet.getOutputDirectory()).request("http://localhost/foo") @@ -223,8 +223,8 @@ public void multipartPostWithParameters() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -269,9 +269,10 @@ public void multipartPostWithContentType() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .build()); + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE) + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 84accb52a..319c37fbe 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -56,7 +56,7 @@ public void prettyPrintXml() throws Exception { equalTo(String .format("%n" + "%n %n%n") - .getBytes())); + .getBytes())); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 40097f3df..52326fdb0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -61,8 +61,8 @@ public class RequestFieldsSnippetTests { public void mapRequestWithFields() throws IOException { this.snippet.expectRequestFields("map-request-with-fields") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a.b", "Number", "one").row("a.c", "String", "two") - .row("a", "Object", "three")); + .row("a.b", "Number", "one").row("a.c", "String", "two").row("a", + "Object", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), @@ -132,11 +132,13 @@ public void missingRequestField() throws IOException { @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ }").build()); + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a.b").description("one").optional())) + .document(new OperationBuilder( + "missing-optional-request-field-with-no-type", + this.snippet.getOutputDirectory()) + .request("http://localhost").content("{ }") + .build()); } @Test @@ -175,14 +177,17 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "request-fields-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test @@ -228,8 +233,8 @@ public void requestFieldsWithListDescription() throws IOException { public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two") - .row("a", "a", "three")); + .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", + "three")); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), @@ -241,7 +246,7 @@ public void xmlRequestFields() throws IOException { .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -283,7 +288,7 @@ public void missingXmlRequestField() throws IOException { .content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index a33a4fec4..6c3eff3dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -94,7 +94,7 @@ public void arrayResponseWithFields() throws IOException { .response() .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); + .build()); } @Test @@ -141,14 +141,17 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response() - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test @@ -172,8 +175,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two") - .row("a", "a", "three")); + .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", + "three")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), @@ -183,7 +186,7 @@ public void xmlResponseFields() throws IOException { .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -199,7 +202,7 @@ public void undocumentedXmlResponseField() throws IOException { .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -215,7 +218,7 @@ public void xmlAttribute() throws IOException { .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -231,7 +234,7 @@ public void missingXmlAttribute() throws IOException { .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -263,7 +266,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -273,15 +276,14 @@ public void documentedXmlAttributesAreRemoved() throws IOException { String.format("The following parts of the payload were not documented:" + "%nbar%n"))); new ResponseFieldsSnippet( - Arrays.asList( - fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", + Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) + .document( + new OperationBuilder("documented-attribute-is-removed", this.snippet.getOutputDirectory()).response() .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -308,7 +310,7 @@ public void missingXmlResponseField() throws IOException { .content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -329,7 +331,7 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 885ba390d..51b1aad96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -79,7 +79,7 @@ public void missingPathParameter() throws IOException { .document(new OperationBuilder("missing-path-parameter", this.snippet.getOutputDirectory()).attribute( "org.springframework.restdocs.urlTemplate", "/") - .build()); + .build()); } @Test @@ -104,11 +104,13 @@ public void pathParameters() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", - "/{a}/{b}") - .build()); + parameterWithName("b").description("two"))) + .document(new OperationBuilder( + "path-parameters", this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -117,11 +119,14 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))).document(new OperationBuilder( - "ignored-path-parameter", this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", - "/{a}/{b}") - .build()); + parameterWithName("b").description("two"))) + .document(new OperationBuilder( + "ignored-path-parameter", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -156,9 +161,10 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { parameterWithName("b").description("two").attributes( key("foo").value("bravo")))).document(new OperationBuilder( "path-parameters-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", - "/{a}/{b}") + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .build()); @@ -176,8 +182,8 @@ public void pathParametersWithCustomAttributes() throws IOException { Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document( new OperationBuilder("path-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index c4b05f355..97b4253bb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -151,13 +151,16 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .attributes(key("foo").value("bravo")))) .document(new OperationBuilder( "request-parameters-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .param("a", "alpha").param("b", "bravo") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha") + .param("b", "bravo").build()); } @Test @@ -172,8 +175,8 @@ public void requestParametersWithCustomAttributes() throws IOException { Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document( new OperationBuilder("request-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index f37ce499a..86dd0f55c 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -217,9 +217,8 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", - responseFields(fieldWithPath("a") - .description("The description"), + .andDo(document("links", responseFields( + fieldWithPath("a").description("The description"), fieldWithPath("links").description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), From 96330b2e30ffe620b1a82ff734e1b797ccfa6cae Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 10:05:20 +0100 Subject: [PATCH 0257/1059] Improve testing of optional links and request and response fields --- .../hypermedia/LinksSnippetTests.java | 6 ++--- .../payload/RequestFieldsSnippetTests.java | 26 +++++++++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 24 +++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 5f07e6ddf..fd0a4c5df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -92,12 +92,12 @@ public void missingLink() throws IOException { } @Test - public void documentedOptionalLink() throws IOException { - this.snippet.expectLinks("documented-optional-link").withContents( + public void presentOptionalLink() throws IOException { + this.snippet.expectLinks("present-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", + .document(new OperationBuilder("present-optional-link", this.snippet.getOutputDirectory()).build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 52326fdb0..216b5ab7b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -129,6 +129,32 @@ public void missingRequestField() throws IOException { .content("{}").build()); } + @Test + public void missingOptionalRequestField() throws IOException { + this.snippet.expectRequestFields("missing-optional-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("missing-optional-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost").content("{}") + .build()); + } + + @Test + public void presentOptionalRequestField() throws IOException { + this.snippet.expectRequestFields("present-optional-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("present-optional-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{\"a\": { \"b\": \"bravo\"}}").build()); + } + @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 6c3eff3dd..1356d7b3b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -121,6 +121,30 @@ public void ignoredResponseField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void missingOptionalResponseField() throws IOException { + this.snippet.expectResponseFields("missing-optional-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("missing-optional-response-field", + this.snippet.getOutputDirectory()).response() + .content("{}").build()); + } + + @Test + public void presentOptionalResponseField() throws IOException { + this.snippet.expectResponseFields("present-optional-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("present-optional-response-field", + this.snippet.getOutputDirectory()).response() + .content("{\"a\": { \"b\": \"bravo\"}}").build()); + } + @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 5b32a3aa9805189190adebcf4e00dbb2a596bb9c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 10:34:02 +0100 Subject: [PATCH 0258/1059] Optional path and req param descriptors should count as documented Previously, optional path and request parameter descriptors were ignored when checking that all of the parameters that are present had been documented. This lead to a false negative if a request or path parameter was present and was documented with an optional descriptor. This commit uses all of the descriptors, not just those that are not optional, when checking for undocumented parameters. Closes gh-228 --- .../request/AbstractParametersSnippet.java | 2 +- .../request/PathParametersSnippetTests.java | 18 ++++++++++++++++++ .../request/RequestParametersSnippetTests.java | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 9d97737d5..f2d5bd99c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -124,7 +124,7 @@ private void verifyParameterDescriptors(Operation operation) { } else { undocumentedParameters = new HashSet<>(actualParameters); - undocumentedParameters.removeAll(expectedParameters); + undocumentedParameters.removeAll(this.descriptorsByName.keySet()); } Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index eac8c9d14..054ed7bcf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -99,6 +99,24 @@ public void missingOptionalPathParameter() throws IOException { "/{a}").build()); } + @Test + public void presentOptionalPathParameter() throws IOException { + this.snippet + .expectPathParameters( + "present-optional-path-parameter") + .withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" + : "`/{a}`", + "Parameter", "Description").row("a", "one")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-path-parameter") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}") + .build()); + } + @Test public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 380f1cc32..2bd69444d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -104,6 +104,17 @@ public void missingOptionalRequestParameter() throws IOException { .build()); } + @Test + public void presentOptionalRequestParameter() throws IOException { + this.snippet.expectRequestParameters("present-optional-request-parameter") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-request-parameter") + .request("http://localhost").param("a", "one").build()); + } + @Test public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 1c95232e6710e2a9a6fb81803b3809bcf55754f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 09:48:11 +0100 Subject: [PATCH 0259/1059] Include optional attribute in model for path and req param descriptors --- .../request/AbstractParametersSnippet.java | 1 + .../RequestParametersSnippetTests.java | 19 +++++++++++++++++++ ...st-parameters-with-optional-column.snippet | 10 ++++++++++ ...st-parameters-with-optional-column.snippet | 5 +++++ 4 files changed, 35 insertions(+) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index f2d5bd99c..31d2913e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -189,6 +189,7 @@ protected Map createModelForDescriptor( Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); model.putAll(descriptor.getAttributes()); return model; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 2bd69444d..4c4c9a82c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -163,6 +163,25 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .build()); } + @Test + public void requestParametersWithOptionalColumn() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-optional-column")); + this.snippet.expectRequestParameters("request-parameters-with-optional-column") + .withContents(tableWithHeader("Parameter", "Optional", "Description") + .row("a", "true", "one").row("b", "false", "two")); + + new RequestParametersSnippet(Arrays.asList( + parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))).document( + operationBuilder("request-parameters-with-optional-column") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").param("a", "alpha") + .param("b", "bravo").build()); + } + @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestParameters("additional-descriptors") diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet new file mode 100644 index 000000000..70847ba1d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Optional|Description + +{{#parameters}} +|{{name}} +|{{optional}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet new file mode 100644 index 000000000..2f9cd0774 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet @@ -0,0 +1,5 @@ +Parameter | Optional | Description +--------- | -------- | ----------- +{{#parameters}} +{{name}} | {{optional}} | {{description}} +{{/parameters}} \ No newline at end of file From 9e84922e096448d6606a9c78d72165a8df954ee3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 11:13:27 +0100 Subject: [PATCH 0260/1059] Upgrade samples to Spring Boot 1.3.3.RELEASE Closes gh-222 --- .../build/SampleBuildConfigurer.groovy | 4 ++-- samples/rest-assured/build.gradle | 7 ++++--- samples/rest-notes-slate/build.gradle | 7 ++++--- .../com/example/notes/RestNotesSlate.java | 8 ++------ samples/rest-notes-spring-data-rest/pom.xml | 3 +-- .../src/main/asciidoc/api-guide.adoc | 9 +++++++++ .../notes/RestNotesSpringDataRest.java | 10 +++------- .../com/example/notes/ApiDocumentation.java | 19 +++++++++++++++---- .../rest-notes-spring-hateoas/build.gradle | 7 ++++--- .../example/notes/RestNotesSpringHateoas.java | 10 +++------- samples/testng/build.gradle | 6 +++--- 11 files changed, 50 insertions(+), 40 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7ce726de8..1be930cf8 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -59,8 +59,8 @@ public class SampleBuildConfigurer { } sampleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), - "springRestdocsVersion = '.*'", - "springRestdocsVersion = '${project.version}'") + "ext\\['spring-restdocs.version'\\] = '.*'", + "ext['spring-restdocs.version'] = '${project.version}'") } } else if (new File(sampleDir, 'pom.xml').isFile()) { diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 9ad942399..f4c79f9b2 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,13 +28,14 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-restassured:$springRestdocsVersion" + testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" } test { diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 79788c3d6..d30ca93ee 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -24,9 +24,10 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-data-rest' @@ -36,7 +37,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java index 32c5ce243..380bebf98 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSlate { public static void main(String[] args) { diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index a874c40c1..5e1735ed4 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.7.RELEASE + 1.3.3.RELEASE @@ -50,7 +50,6 @@ org.springframework.restdocs spring-restdocs-mockmvc - ${spring-restdocs.version} test diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 50498df41..4f7697d70 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -138,6 +138,10 @@ include::{snippets}/notes-list-example/curl-request.adoc[] include::{snippets}/notes-list-example/http-response.adoc[] +[[resources-notes-list-links]] +==== Links + +include::{snippets}/notes-list-example/links.adoc[] [[resources-notes-create]] @@ -183,6 +187,11 @@ include::{snippets}/tags-list-example/curl-request.adoc[] include::{snippets}/tags-list-example/http-response.adoc[] +[[resources-tags-list-links]] +==== Links + +include::{snippets}/tags-list-example/links.adoc[] + [[resources-tags-create]] diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java index 74b98dd0c..9df142549 100644 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSpringDataRest { public static void main(String[] args) { 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 cbcaeb3fc..2db8fcb40 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 @@ -133,8 +133,12 @@ public void notesListExample() throws Exception { this.mockMvc.perform(get("/notes")) .andExpect(status().isOk()) .andDo(document("notes-list-example", + links( + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + fieldWithPath("_embedded.notes").description("An array of <>"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -195,9 +199,11 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(print()) .andDo(document("note-get-example", links( - linkWithRel("self").description("This <>"), + linkWithRel("self").description("Canonical link for this <>"), + linkWithRel("note").description("This <>"), linkWithRel("tags").description("This note's tags")), responseFields( fieldWithPath("title").description("The title of the note"), @@ -217,8 +223,12 @@ public void tagsListExample() throws Exception { this.mockMvc.perform(get("/tags")) .andExpect(status().isOk()) .andDo(document("tags-list-example", + links( + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + fieldWithPath("_embedded.tags").description("An array of <>"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -295,7 +305,8 @@ public void tagGetExample() throws Exception { .andExpect(jsonPath("name", is(tag.get("name")))) .andDo(document("tag-get-example", links( - linkWithRel("self").description("This <>"), + linkWithRel("self").description("Canonical link for this <>"), + linkWithRel("tag").description("This <>"), linkWithRel("notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 555d25ec3..7941829f9 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,9 +28,10 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-hateoas' @@ -40,7 +41,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java index c51ecee0d..9ad461587 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 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. @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSpringHateoas { public static void main(String[] args) { diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index a6eeb77af..7683e955d 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,8 +28,8 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' @@ -37,7 +37,7 @@ dependencies { exclude group: 'junit', module: 'junit;' } testCompile 'org.testng:testng:6.9.10' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { From e37d0ff560eb409fbdbedfd82cdb7859043578ea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 11:33:05 +0100 Subject: [PATCH 0261/1059] Use Maven Central rather than JCenter --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b6e05c519..8de250007 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { - jcenter() + mavenCentral() maven { url 'https://repo.spring.io/plugins-release' } maven { url 'https://plugins.gradle.org/m2/' } } @@ -15,7 +15,7 @@ buildscript { allprojects { group = 'org.springframework.restdocs' repositories { - jcenter() + mavenCentral() } } From 99b84e40f4980e4f95696fadedc351bf58fb1329 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 12:29:17 +0100 Subject: [PATCH 0262/1059] Output stacktrace when Gradle samples fails to build --- .../restdocs/build/SampleBuildConfigurer.groovy | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 5ddd17ab1..2d56e5a26 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -16,12 +16,14 @@ package org.springframework.restdocs.build +import org.gradle.StartParameter import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Copy import org.gradle.api.tasks.GradleBuild +import org.gradle.logging.ShowStacktrace public class SampleBuildConfigurer { @@ -85,9 +87,12 @@ public class SampleBuildConfigurer { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" gradleBuild.group = "Build" - gradleBuild.dir = this.workingDir - gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies + StartParameter startParameter = new StartParameter() + startParameter.showStacktrace = ShowStacktrace.ALWAYS + startParameter.taskNames = ['clean', 'build'] + startParameter.currentDir = new File(this.workingDir) + gradleBuild.startParameter = startParameter gradleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), From 6a5fd53910b3a2341d62ec657e2f0a9f1491ac56 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 12:47:22 +0100 Subject: [PATCH 0263/1059] Update Asciidoctor dependencies --- docs/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/build.gradle b/docs/build.gradle index 5241018c0..681796107 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.asciidoctor.convert' version '1.5.2' + id 'org.asciidoctor.convert' version '1.5.3' } dependencies { diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 6e057717d..95427ccbb 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -73,7 +73,7 @@ org.asciidoctor asciidoctor-maven-plugin - 1.5.2.1 + 1.5.3 generate-docs From 40a12127d7cf34e3d4f338b5549304183f7b897d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:24:32 +0100 Subject: [PATCH 0264/1059] Alternative approach to getting a stacktrace for a sample build failure --- .travis.yml | 2 +- .../restdocs/build/SampleBuildConfigurer.groovy | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index d84c55194..66f456e77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ language: java jdk: - oraclejdk7 script: - - "./gradlew build buildSamples" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace" \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 2d56e5a26..5ddd17ab1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -16,14 +16,12 @@ package org.springframework.restdocs.build -import org.gradle.StartParameter import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Copy import org.gradle.api.tasks.GradleBuild -import org.gradle.logging.ShowStacktrace public class SampleBuildConfigurer { @@ -87,12 +85,9 @@ public class SampleBuildConfigurer { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" gradleBuild.group = "Build" + gradleBuild.dir = this.workingDir + gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies - StartParameter startParameter = new StartParameter() - startParameter.showStacktrace = ShowStacktrace.ALWAYS - startParameter.taskNames = ['clean', 'build'] - startParameter.currentDir = new File(this.workingDir) - gradleBuild.startParameter = startParameter gradleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), From 2417e9588d6f5222c990fbba39069f033be7e511 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:37:36 +0100 Subject: [PATCH 0265/1059] Disable Gradle daemon when building on Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 66f456e77..69d0bdad0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,7 @@ sudo: false language: java jdk: - oraclejdk7 +env: + - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - "./gradlew build buildSamples --stacktrace" \ No newline at end of file From 4d5f0df17e3a21b050367f02dcc13fe139181bda Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:49:08 +0100 Subject: [PATCH 0266/1059] Try to coax some useful diagnostics out of failing Travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 69d0bdad0..7736261c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ jdk: env: - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --debug" \ No newline at end of file From 4a67b80b15b5d80048b44d63d9708c885f6b7455 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:53:57 +0100 Subject: [PATCH 0267/1059] Try --info. Travis can't cope with volume produced by --debug --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7736261c1..0b31aca57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ jdk: env: - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace --debug" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --info" \ No newline at end of file From bf9d977321f81f47ae99793ac1baebf1589306b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:00:09 +0100 Subject: [PATCH 0268/1059] Try a different approach to switching off the Gradle daemon on Travis --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b31aca57..8de78ff2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,5 @@ sudo: false language: java jdk: - oraclejdk7 -env: - - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace --info" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --info --no-daemon" \ No newline at end of file From a0236f67eb0a43a9902749d956c720e8ba52463a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:06:12 +0100 Subject: [PATCH 0269/1059] Build using Java 8 on Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8de78ff2c..db7f567de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: java jdk: - - oraclejdk7 + - oraclejdk8 script: - "./gradlew build buildSamples --stacktrace --info --no-daemon" \ No newline at end of file From f23d947cb520b81181dee79c75e055d7d5bf9dff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:11:49 +0100 Subject: [PATCH 0270/1059] Use Spring Boot 1.3.3 to test REST Assured module --- spring-restdocs-restassured/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 5f495f507..c9385ea82 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -10,7 +10,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.3.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 75ea2101872c51f459dd1fc47cf1b2ebf7bdad7c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:29:20 +0100 Subject: [PATCH 0271/1059] Build against Spring Framework 4.2.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 419d5c8de..e08c78a52 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.1.8.RELEASE' + springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", From 60a02a431fde88cd5d2d3174a2d54d95bfd520fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:37:58 +0100 Subject: [PATCH 0272/1059] Improve documentation of project's requirements Closes gh-217 --- README.md | 2 +- docs/src/docs/asciidoc/getting-started.adoc | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aaa0c6dcc..a1488ceb0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ To learn more about Spring REST Docs, please consult the [reference documentatio ## Building from source -Spring REST Docs requires Java 7 or later and is built using [Gradle][10]: +You will need Java 7 or later to build Spring REST Docs. It is built using [Gradle][10]: ``` ./gradlew build diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8c0dba9d1..eb9c1fe5f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -39,7 +39,18 @@ If you want to jump straight in, a number of sample applications are available: |=== +[[getting-started-requirements]] +=== Requirements +Spring REST Docs has the following minimum requirements: + +- Java 7 +- Spring Framework 4.2 + +Additionally, the `spring-restdocs-restassured` module has the following minimum +requirements: + +- REST Assured 2.8 [[getting-started-build-configuration]] === Build configuration From d362f3f41f1307700e2d1c2f3710c9a2b02b767c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:47:03 +0100 Subject: [PATCH 0273/1059] Document support for relaxed snippets Closes gh-223 --- .../docs/asciidoc/documenting-your-api.adoc | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9242fcbc3..7e53f1d99 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -49,6 +49,11 @@ response and the link has not been marked as optional. If you do not want to document a link, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. +Links can also be documented in a relaxed mode where any undocumented links will not cause +a test failure. To do so, use the `relaxedLinks` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the links. + [[documenting-your-api-hypermedia-link-formats]] @@ -144,6 +149,12 @@ treated as having been documented. If you do not want to document a field, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. +Fields can also be documented in a relaxed mode where any undocumented fields will not +cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields` +methods on +`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the payload. + TIP: By default, Spring REST Docs will assume that the payload you are documenting is JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. @@ -454,6 +465,12 @@ If you do not want to document a request parameter, you can mark it as ignored. prevent it from appearing in the generated snippet while avoiding the failure described above. +Request parameters can also be documented in a relaxed mode where any undocumented +parameters will not cause a test failure. To do so, use the `relaxedRequestParameters` +method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +request parameters. + [[documenting-your-api-path-parameters]] @@ -498,6 +515,12 @@ When documenting path parameters, the test will fail if an undocumented path par is used in the request. Similarly, the test will also fail if a documented path parameter is not found in the request and the path parameter has not been marked as optional. +Path parameters can also be documented in a relaxed mode where any undocumented +parameters will not cause a test failure. To do so, use the `relaxedPathParameters` +method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +path parameters. + If you do not want to document a path parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. From 63a6ad2c9166133e53384747f2581632ff172bf2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 May 2016 14:50:39 +0100 Subject: [PATCH 0274/1059] Improve readability by using monospaced font in various snippets This commit updates a number of the snippets to use a monospaced font for parts of their output. The changes are: - Links snippet: rel - Path parameters: name - Request fields: path and type - Request headers: name - Request parameters: name - Response fields: path and type - Response headers: name Closes gh-230 --- .../asciidoctor/default-links.snippet | 2 +- .../default-path-parameters.snippet | 2 +- .../default-request-fields.snippet | 4 +- .../default-request-headers.snippet | 2 +- .../default-request-parameters.snippet | 2 +- .../default-response-fields.snippet | 4 +- .../default-response-headers.snippet | 2 +- .../templates/markdown/default-links.snippet | 2 +- .../markdown/default-path-parameters.snippet | 2 +- .../markdown/default-request-fields.snippet | 2 +- .../markdown/default-request-headers.snippet | 2 +- .../default-request-parameters.snippet | 2 +- .../markdown/default-response-fields.snippet | 2 +- .../markdown/default-response-headers.snippet | 2 +- .../headers/RequestHeadersSnippetTests.java | 20 ++++--- .../headers/ResponseHeadersSnippetTests.java | 18 +++--- .../hypermedia/LinksSnippetTests.java | 20 +++---- .../payload/RequestFieldsSnippetTests.java | 37 ++++++------ .../payload/ResponseFieldsSnippetTests.java | 57 ++++++++++--------- .../request/PathParametersSnippetTests.java | 14 ++--- .../RequestParametersSnippetTests.java | 20 +++---- 21 files changed, 112 insertions(+), 106 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index b8b452903..132643d8e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,7 +2,7 @@ |Relation|Description {{#links}} -|{{rel}} +|`{{rel}}` |{{description}} {{/links}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index 66db38621..a6cc9f8d4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -3,7 +3,7 @@ |Parameter|Description {{#parameters}} -|{{name}} +|`{{name}}` |{{description}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 497f5a03a..41e763e74 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{path}} -|{{type}} +|`{{path}}` +|`{{type}}` |{{description}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index 5f14875dc..f3d660355 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{name}} +|`{{name}}` |{{description}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index 067e1fb83..f338b345f 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,7 +2,7 @@ |Parameter|Description {{#parameters}} -|{{name}} +|`{{name}}` |{{description}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 497f5a03a..41e763e74 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{path}} -|{{type}} +|`{{path}}` +|`{{type}}` |{{description}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index 5f14875dc..f3d660355 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{name}} +|`{{name}}` |{{description}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet index 116d8eabe..8c690d0f1 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet @@ -1,5 +1,5 @@ Relation | Description -------- | ----------- {{#links}} -{{rel}} | {{description}} +`{{rel}}` | {{description}} {{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet index f1811879f..76dfc572e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet @@ -3,5 +3,5 @@ Parameter | Description --------- | ----------- {{#parameters}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet index e8d2957f3..27a4e4379 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet @@ -1,5 +1,5 @@ Path | Type | Description ---- | ---- | ----------- {{#fields}} -{{path}} | {{type}} | {{description}} +`{{path}}` | `{{type}}` | {{description}} {{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet index f8c5ed27e..7bbe694df 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet @@ -1,5 +1,5 @@ Name | Description ---- | ----------- {{#headers}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet index 09a8a1992..681daaa8e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet @@ -1,5 +1,5 @@ Parameter | Description --------- | ----------- {{#parameters}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet index e8d2957f3..27a4e4379 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet @@ -1,5 +1,5 @@ Path | Type | Description ---- | ---- | ----------- {{#fields}} -{{path}} | {{type}} | {{description}} +`{{path}}` | `{{type}}` | {{description}} {{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet index f8c5ed27e..7bbe694df 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet @@ -1,5 +1,5 @@ Name | Description ---- | ----------- {{#headers}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 2f97aee88..7a0c9edd0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -50,10 +50,11 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { this.snippet.expectRequestHeaders("request-with-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Accept", "two").row("Accept-Encoding", "three") - .row("Accept-Language", "four").row("Cache-Control", "five") - .row("Connection", "six")); + .withContents(tableWithHeader("Name", "Description") + .row("`X-Test`", "one").row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four").row("`Cache-Control`", "five") + .row("`Connection`", "six")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -74,7 +75,7 @@ public void requestWithHeaders() throws IOException { public void caseInsensitiveRequestHeaders() throws IOException { this.snippet.expectRequestHeaders("case-insensitive-request-headers") .withContents( - tableWithHeader("Name", "Description").row("X-Test", "one")); + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(operationBuilder("case-insensitive-request-headers") @@ -143,10 +144,11 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestHeaders("additional-descriptors") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Accept", "two").row("Accept-Encoding", "three") - .row("Accept-Language", "four").row("Cache-Control", "five") - .row("Connection", "six")); + .withContents(tableWithHeader("Name", "Description") + .row("`X-Test`", "one").row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four").row("`Cache-Control`", "five") + .row("`Connection`", "six")); HeaderDocumentation .requestHeaders(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 463f631c4..d7bf553ac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -49,10 +49,10 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Content-Type", "two").row("Etag", "three") - .row("Cache-Control", "five").row("Vary", "six")); + this.snippet.expectResponseHeaders("response-headers").withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one") + .row("`Content-Type`", "two").row("`Etag`", "three") + .row("`Cache-Control`", "five").row("`Vary`", "six")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), @@ -73,7 +73,7 @@ public void responseWithHeaders() throws IOException { public void caseInsensitiveResponseHeaders() throws IOException { this.snippet.expectResponseHeaders("case-insensitive-response-headers") .withContents( - tableWithHeader("Name", "Description").row("X-Test", "one")); + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(operationBuilder("case-insensitive-response-headers") @@ -136,10 +136,10 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders("additional-descriptors") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Content-Type", "two").row("Etag", "three") - .row("Cache-Control", "five").row("Vary", "six")); + this.snippet.expectResponseHeaders("additional-descriptors").withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one") + .row("`Content-Type`", "two").row("`Etag`", "three") + .row("`Cache-Control`", "five").row("`Vary`", "six")); HeaderDocumentation .responseHeaders(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index cf128d212..46f73b303 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,7 +47,7 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { this.snippet.expectLinks("ignored-link").withContents( - tableWithHeader("Relation", "Description").row("b", "Link b")); + tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -59,7 +59,7 @@ public void ignoredLink() throws IOException { @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { this.snippet.expectLinks("ignore-all-undocumented").withContents( - tableWithHeader("Relation", "Description").row("b", "Link b")); + tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -70,7 +70,7 @@ public void allUndocumentedLinksCanBeIgnored() throws IOException { @Test public void presentOptionalLink() throws IOException { this.snippet.expectLinks("present-optional-link").withContents( - tableWithHeader("Relation", "Description").row("foo", "bar")); + tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) .document(operationBuilder("present-optional-link").build()); @@ -79,7 +79,7 @@ public void presentOptionalLink() throws IOException { @Test public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( - tableWithHeader("Relation", "Description").row("foo", "bar")); + tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) .document(operationBuilder("missing-optional-link").build()); @@ -88,8 +88,8 @@ public void missingOptionalLink() throws IOException { @Test public void documentedLinks() throws IOException { this.snippet.expectLinks("documented-links") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "two")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -101,8 +101,8 @@ public void documentedLinks() throws IOException { @Test public void linkDescriptionFromTitleInPayload() throws IOException { this.snippet.expectLinks("link-description-from-title-in-payload") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "Link b")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), @@ -162,8 +162,8 @@ public void linksWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectLinks("additional-descriptors") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index b7e0c872d..eac0b8438 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -51,8 +51,8 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { public void mapRequestWithFields() throws IOException { this.snippet.expectRequestFields("map-request-with-fields") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a.b", "Number", "one").row("a.c", "String", "two").row("a", - "Object", "three")); + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), @@ -67,8 +67,9 @@ public void mapRequestWithFields() throws IOException { public void arrayRequestWithFields() throws IOException { this.snippet.expectRequestFields("array-request-with-fields") .withContents(tableWithHeader("Path", "Type", "Description") - .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") - .row("[]a", "Object", "three")); + .row("`[]a.b`", "`Number`", "one") + .row("`[]a.c`", "`String`", "two") + .row("`[]a`", "`Object`", "three")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), @@ -83,8 +84,8 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { this.snippet.expectRequestFields("ignored-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) @@ -96,8 +97,8 @@ public void ignoredRequestField() throws IOException { @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { this.snippet.expectRequestFields("ignore-all-undocumented") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true).document( operationBuilder("ignore-all-undocumented") @@ -108,8 +109,8 @@ public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestField() throws IOException { this.snippet.expectRequestFields("missing-optional-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("missing-optional-request-field") @@ -119,8 +120,8 @@ public void missingOptionalRequestField() throws IOException { @Test public void presentOptionalRequestField() throws IOException { this.snippet.expectRequestFields("present-optional-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("present-optional-request-field") @@ -179,8 +180,8 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", - "three")); + .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", + "`a`", "three")); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), @@ -199,8 +200,8 @@ public void xmlRequestFields() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectRequestFields("additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a.b", "Number", "one").row("a.c", "String", "two").row("a", - "Object", "three")); + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); PayloadDocumentation .requestFields(fieldWithPath("a.b").description("one"), @@ -215,8 +216,8 @@ public void additionalDescriptors() throws IOException { public void prefixedAdditionalDescriptors() throws IOException { this.snippet.expectRequestFields("prefixed-additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "Object", "one").row("a.b", "Number", "two").row("a.c", - "String", "three")); + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index fb97e3511..574899a09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -51,10 +51,11 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { public void mapResponseWithFields() throws IOException { this.snippet.expectResponseFields("map-response-with-fields") .withContents(tableWithHeader("Path", "Type", "Description") - .row("id", "Number", "one").row("date", "String", "two") - .row("assets", "Array", "three").row("assets[]", "Object", "four") - .row("assets[].id", "Number", "five") - .row("assets[].name", "String", "six")); + .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Object`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), @@ -72,8 +73,9 @@ public void mapResponseWithFields() throws IOException { public void arrayResponseWithFields() throws IOException { this.snippet.expectResponseFields("array-response-with-fields") .withContents(tableWithHeader("Path", "Type", "Description") - .row("[]a.b", "Number", "one").row("[]a.c", "String", "two") - .row("[]a", "Object", "three")); + .row("`[]a.b`", "`Number`", "one") + .row("`[]a.c`", "`String`", "two") + .row("`[]a`", "`Object`", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))).document( @@ -86,8 +88,8 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { this.snippet.expectResponseFields("array-response") - .withContents(tableWithHeader("Path", "Type", "Description").row("[]", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) .document(operationBuilder("array-response").response() .content("[\"a\", \"b\", \"c\"]").build()); @@ -96,8 +98,8 @@ public void arrayResponse() throws IOException { @Test public void ignoredResponseField() throws IOException { this.snippet.expectResponseFields("ignored-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) @@ -108,8 +110,8 @@ public void ignoredResponseField() throws IOException { @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { this.snippet.expectResponseFields("ignore-all-undocumented") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("b").description("Field b")), true) @@ -136,8 +138,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { @Test public void missingOptionalResponseField() throws IOException { this.snippet.expectResponseFields("missing-optional-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("missing-optional-response-field") @@ -147,8 +149,8 @@ public void missingOptionalResponseField() throws IOException { @Test public void presentOptionalResponseField() throws IOException { this.snippet.expectResponseFields("present-optional-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())).document( operationBuilder("present-optional-response-field").response() @@ -186,8 +188,8 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", - "three")); + .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", + "`a`", "three")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), @@ -204,7 +206,7 @@ public void xmlResponseFields() throws IOException { public void xmlAttribute() throws IOException { this.snippet.expectResponseFields("xml-attribute") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "b", "one").row("a/@id", "c", "two")); + .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) @@ -220,7 +222,7 @@ public void xmlAttribute() throws IOException { public void missingOptionalXmlAttribute() throws IOException { this.snippet.expectResponseFields("missing-optional-xml-attribute") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "b", "one").row("a/@id", "c", "two")); + .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c").optional())) @@ -235,7 +237,7 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); + tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("a"))) .document(operationBuilder("undocumented-attribute").response() @@ -249,10 +251,11 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectResponseFields("additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("id", "Number", "one").row("date", "String", "two") - .row("assets", "Array", "three").row("assets[]", "Object", "four") - .row("assets[].id", "Number", "five") - .row("assets[].name", "String", "six")); + .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Object`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); PayloadDocumentation .responseFields(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), @@ -270,8 +273,8 @@ public void additionalDescriptors() throws IOException { public void prefixedAdditionalDescriptors() throws IOException { this.snippet.expectResponseFields("prefixed-additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "Object", "one").row("a.b", "Number", "two").row("a.c", - "String", "three")); + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 054ed7bcf..c5f700654 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -51,7 +51,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) .document(operationBuilder("path-parameters").attribute( @@ -62,7 +62,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { this.snippet.expectPathParameters("ignored-path-parameter").withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -74,7 +74,7 @@ public void ignoredPathParameter() throws IOException { @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { this.snippet.expectPathParameters("ignore-all-undocumented").withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) @@ -91,7 +91,7 @@ public void missingOptionalPathParameter() throws IOException { .withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("a", "one").row("b", "two")); + "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two").optional())).document( operationBuilder("missing-optional-path-parameter").attribute( @@ -107,7 +107,7 @@ public void presentOptionalPathParameter() throws IOException { .withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("a", "one")); + "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) .document(operationBuilder("present-optional-path-parameter") @@ -122,7 +122,7 @@ public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") .withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))).document( operationBuilder("path-parameters-with-query-string").attribute( @@ -183,7 +183,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectPathParameters("additional-descriptors").withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) .document(operationBuilder("additional-descriptors") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 4c4c9a82c..94135a3bf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,8 +48,8 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { this.snippet.expectRequestParameters("request-parameters") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) @@ -62,7 +62,7 @@ public void requestParameters() throws IOException { public void requestParameterWithNoValue() throws IOException { this.snippet.expectRequestParameters("request-parameter-with-no-value") .withContents( - tableWithHeader("Parameter", "Description").row("a", "one")); + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) .document(operationBuilder("request-parameter-with-no-value") @@ -72,7 +72,7 @@ public void requestParameterWithNoValue() throws IOException { @Test public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( - tableWithHeader("Parameter", "Description").row("b", "two")); + tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) .document(operationBuilder("ignored-request-parameter") @@ -83,7 +83,7 @@ public void ignoredRequestParameter() throws IOException { @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( - tableWithHeader("Parameter", "Description").row("b", "two")); + tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) .document(operationBuilder("ignore-all-undocumented") @@ -94,8 +94,8 @@ public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { @Test public void missingOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("missing-optional-request-parameter") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))).document( @@ -108,7 +108,7 @@ public void missingOptionalRequestParameter() throws IOException { public void presentOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("present-optional-request-parameter") .withContents( - tableWithHeader("Parameter", "Description").row("a", "one")); + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) .document(operationBuilder("present-optional-request-parameter") @@ -185,8 +185,8 @@ public void requestParametersWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestParameters("additional-descriptors") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) .document(operationBuilder("additional-descriptors") From fc3ae3b4a285e112e59b14423eb75f9744ac467b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 May 2016 15:21:16 +0100 Subject: [PATCH 0275/1059] Add support for documenting the parts of a multipart request Closes gh-161 --- .../docs/asciidoc/documenting-your-api.adoc | 49 +++++ .../com/example/mockmvc/RequestParts.java | 41 ++++ .../com/example/restassured/RequestParts.java | 42 ++++ .../request/RequestDocumentation.java | 90 ++++++++ .../request/RequestPartDescriptor.java | 73 +++++++ .../restdocs/request/RequestPartsSnippet.java | 206 ++++++++++++++++++ .../asciidoctor/default-request-parts.snippet | 9 + .../markdown/default-request-parts.snippet | 5 + .../RequestPartsSnippetFailureTests.java | 83 +++++++ .../request/RequestPartsSnippetTests.java | 183 ++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 5 + .../request-parts-with-extra-column.snippet | 10 + ...request-parts-with-optional-column.snippet | 10 + .../request-parts-with-title.snippet | 10 + .../request-parts-with-extra-column.snippet | 5 + ...request-parts-with-optional-column.snippet | 5 + .../markdown/request-parts-with-title.snippet | 6 + ...kMvcRestDocumentationIntegrationTests.java | 31 +++ .../restdocs/templates/request-parts.snippet | 9 + ...uredRestDocumentationIntegrationTests.java | 18 ++ 20 files changed, 890 insertions(+) create mode 100644 docs/src/test/java/com/example/mockmvc/RequestParts.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParts.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet create mode 100644 spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7e53f1d99..d1a27442b 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -527,6 +527,55 @@ above. +[[documenting-your-api-request-parts]] +=== Request parts + +The parts of a multipart request can be documenting using `requestParts`. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestParts.java[tags=request-parts] +---- +<1> Perform a `POST` request with a single part named `file`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses + the static `requestParts` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the part named `file`. Uses the static `partWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses + the static `requestParts` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the part named `file`. Uses the static `partWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Configure the request with the part named `file`. +<4> Perform the `POST` request to `/upload`. + +The result is a snippet named `request-parts.adoc` that contains a table describing the +request parts that are supported by the resource. + +When documenting request parts, the test will fail if an undocumented part is used in the +request. Similarly, the test will also fail if a documented part is not found in the +request and the part has not been marked as optional. + +Request parts can also be documented in a relaxed mode where any undocumented +parts will not cause a test failure. To do so, use the `relaxedRequestParts` method on +`org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +request parts. + +If you do not want to document a request part, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. + + + [[documenting-your-api-http-headers]] === HTTP headers diff --git a/docs/src/test/java/com/example/mockmvc/RequestParts.java b/docs/src/test/java/com/example/mockmvc/RequestParts.java new file mode 100644 index 000000000..90210d50f --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestParts.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestParts { + + private MockMvc mockMvc; + + public void upload() throws Exception { + // tag::request-parts[] + this.mockMvc.perform(fileUpload("/upload").file("file", "example".getBytes())) // <1> + .andExpect(status().isOk()) + .andDo(document("upload", requestParts( // <2> + partWithName("file").description("The file to upload")) // <3> + )); + // end::request-parts[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java new file mode 100644 index 000000000..c6a3f8c1f --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParts.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParts { + + private RequestSpecification spec; + + public void upload() throws Exception { + // tag::request-parts[] + RestAssured.given(this.spec) + .filter(document("users", requestParts( // <1> + partWithName("file").description("The file to upload")))) // <2> + .multiPart("file", "example") // <3> + .when().post("/upload") // <4> + .then().statusCode(is(200)); + // end::request-parts[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 4c4aee73d..4fec764af 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -43,6 +43,17 @@ public static ParameterDescriptor parameterWithName(String name) { return new ParameterDescriptor(name); } + /** + * Creates a {@link RequestPartDescriptor} that describes a request part with the + * given {@code name}. + * + * @param name The name of the request part + * @return a {@link RequestPartDescriptor} ready for further configuration + */ + public static RequestPartDescriptor partWithName(String name) { + return new RequestPartDescriptor(name); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The parameters will be documented using the given @@ -207,4 +218,83 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parts will be documented using the given {@code descriptors}. + *

            + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

            + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parameters will be documented using the given {@code descriptors}. + *

            + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet relaxedRequestParts( + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), true); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

            + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

            + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(Map attributes, + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

            + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestPartsSnippet relaxedRequestParts(Map attributes, + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java new file mode 100644 index 000000000..789649607 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2016 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.request; + +import org.springframework.restdocs.snippet.IgnorableDescriptor; + +/** + * A descriptor of a request part. + * + * @author Andy Wilkinson + * @see RequestDocumentation#partWithName + */ +public class RequestPartDescriptor extends IgnorableDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code RequestPartDescriptor} describing the request part with the + * given {@code name}. + * + * @param name the name of the request part + */ + protected RequestPartDescriptor(String name) { + this.name = name; + } + + /** + * Marks the request part as optional. + * + * @return {@code this} + */ + public final RequestPartDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name of the request part being described by this descriptor. + * + * @return the name of the parameter + */ + public final String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described request part is optional, otherwise + * {@code false}. + * + * @return {@code true} if the described request part is optional, otherwise + * {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java new file mode 100644 index 000000000..1a5d30597 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -0,0 +1,206 @@ +/* + * Copyright 2014-2016 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.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * A {@link Snippet} that documents the request parts supported by a RESTful resource. + * + * @author Andy Wilkinson + * @see RequestDocumentation#requestParts(RequestPartDescriptor...) + * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) + * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) + * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) + */ +public class RequestPartsSnippet extends TemplatedSnippet { + + private final Map descriptorsByName = new LinkedHashMap<>(); + + private final boolean ignoreUndocumentedParts; + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. Undocumented parts will trigger a failure. + * + * @param descriptors the parameter descriptors + */ + protected RequestPartsSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. If {@code ignoreUndocumentedParts} is + * {@code true}, undocumented parts will be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParts whether undocumented parts should be ignored + */ + protected RequestPartsSnippet(List descriptors, + boolean ignoreUndocumentedParts) { + this(descriptors, null, ignoreUndocumentedParts); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. The given {@code attributes} will be included + * in the model during template rendering. Undocumented parts will trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected RequestPartsSnippet(List descriptors, + Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. The given {@code attributes} will be included + * in the model during template rendering. If {@code ignoreUndocumentedParts} is + * {@code true}, undocumented parts will be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParts whether undocumented parts should be ignored + */ + protected RequestPartsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParts) { + super("request-parts", attributes); + for (RequestPartDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName(), + "Request part descriptors must have a name"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), + "The descriptor for request part '" + descriptor.getName() + + "' must either have a description or be marked as " + + "ignored"); + } + this.descriptorsByName.put(descriptor.getName(), descriptor); + } + this.ignoreUndocumentedParts = ignoreUndocumentedParts; + } + + /** + * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes + * and its descriptors combined with the given {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.descriptorsByName.values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestPartsSnippet(combinedDescriptors, this.getAttributes()); + } + + @Override + protected Map createModel(Operation operation) { + verifyRequestPartDescriptors(operation); + Map model = new HashMap<>(); + List> requestParts = new ArrayList<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + RequestPartDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + requestParts.add(createModelForDescriptor(descriptor)); + } + } + model.put("requestParts", requestParts); + return model; + } + + private void verifyRequestPartDescriptors(Operation operation) { + Set actualRequestParts = extractActualRequestParts(operation); + Set expectedRequestParts = new HashSet<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + if (!entry.getValue().isOptional()) { + expectedRequestParts.add(entry.getKey()); + } + } + Set undocumentedRequestParts; + if (this.ignoreUndocumentedParts) { + undocumentedRequestParts = Collections.emptySet(); + } + else { + undocumentedRequestParts = new HashSet<>(actualRequestParts); + undocumentedRequestParts.removeAll(this.descriptorsByName.keySet()); + } + + Set missingRequestParts = new HashSet<>(expectedRequestParts); + missingRequestParts.removeAll(actualRequestParts); + + if (!undocumentedRequestParts.isEmpty() || !missingRequestParts.isEmpty()) { + verificationFailed(undocumentedRequestParts, missingRequestParts); + } + } + + private Set extractActualRequestParts(Operation operation) { + Set actualRequestParts = new HashSet<>(); + for (OperationRequestPart requestPart : operation.getRequest().getParts()) { + actualRequestParts.add(requestPart.getName()); + } + return actualRequestParts; + } + + private void verificationFailed(Set undocumentedRequestParts, + Set missingRequestParts) { + String message = ""; + if (!undocumentedRequestParts.isEmpty()) { + message += "Request parts with the following names were not documented: " + + undocumentedRequestParts; + } + if (!missingRequestParts.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Request parts with the following names were not found in " + + "the request: " + missingRequestParts; + } + throw new SnippetException(message); + } + + private Map createModelForDescriptor( + RequestPartDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet new file mode 100644 index 000000000..3ed4773b5 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -0,0 +1,9 @@ +|=== +|Part|Description + +{{#requestParts}} +|`{{name}}` +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet new file mode 100644 index 000000000..b313f2d3e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet @@ -0,0 +1,5 @@ +Part | Description +---- | ----------- +{{#requestParts}} +`{{name}}` | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java new file mode 100644 index 000000000..93de349a5 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2016 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.request; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; + +/** + * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or + * undocumented request parts. + * + * @author Andy Wilkinson + */ +public class RequestPartsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo( + "Request parts with the following names were" + " not documented: [a]")); + new RequestPartsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-part", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("a", "alpha".getBytes()).build()); + } + + @Test + public void missingPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were" + + " not found in the request: [a]")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(new OperationBuilder("missing-part", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + + @Test + public void undocumentedAndMissingParts() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were" + + " not documented: [b]. Request parts with the following" + + " names were not found in the request: [a]")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(new OperationBuilder("undocumented-and-missing-parts", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("b", "bravo".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java new file mode 100644 index 000000000..6ce2a09a2 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014-2016 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.request; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartsSnippet}. + * + * @author Andy Wilkinson + */ +public class RequestPartsSnippetTests extends AbstractSnippetTests { + + public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestParts() throws IOException { + this.snippet.expectRequestParts("request-parts") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), + partWithName("b").description("two"))) + .document(operationBuilder("request-parts") + .request("http://localhost").part("a", "bravo".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void ignoredRequestPart() throws IOException { + this.snippet.expectRequestParts("ignored-request-part") + .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), + partWithName("b").description("two"))) + .document(operationBuilder("ignored-request-part") + .request("http://localhost").part("a", "bravo".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { + this.snippet.expectRequestParts("ignore-all-undocumented") + .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) + .document(operationBuilder("ignore-all-undocumented") + .request("http://localhost").part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void missingOptionalRequestPart() throws IOException { + this.snippet.expectRequestParts("missing-optional-request-parts") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional(), + partWithName("b").description("two"))).document( + operationBuilder("missing-optional-request-parts") + .request("http://localhost") + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void presentOptionalRequestPart() throws IOException { + this.snippet.expectRequestParts("present-optional-request-part") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-request-part") + .request("http://localhost").part("a", "one".getBytes()) + .build()); + } + + @Test + public void requestPartsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-title")); + this.snippet.expectRequestParts("request-parts-with-custom-attributes") + .withContents(containsString("The title")); + + new RequestPartsSnippet( + Arrays.asList( + partWithName("a").description("one") + .attributes(key("foo").value("alpha")), + partWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) + .document(operationBuilder("request-parts-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").part("a", "alpha".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void requestPartsWithCustomDescriptorAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-extra-column")); + this.snippet.expectRequestParts("request-parts-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Part", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); + + new RequestPartsSnippet(Arrays.asList( + partWithName("a").description("one") + .attributes(key("foo").value("alpha")), + partWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(operationBuilder( + "request-parts-with-custom-descriptor-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void requestPartsWithOptionalColumn() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-optional-column")); + this.snippet.expectRequestParts("request-parts-with-optional-column") + .withContents(tableWithHeader("Part", "Optional", "Description") + .row("a", "true", "one").row("b", "false", "two")); + + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional(), + partWithName("b").description("two"))).document( + operationBuilder("request-parts-with-optional-column") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestParts("additional-descriptors") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + RequestDocumentation.requestParts(partWithName("a").description("one")) + .and(partWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 2799f1cc0..bc1c77856 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -126,6 +126,11 @@ public ExpectedSnippet expectPathParameters(String name) { return this; } + public ExpectedSnippet expectRequestParts(String name) { + expect(name, "request-parts"); + return this; + } + private ExpectedSnippet expect(String name, String type) { this.expectedName = name; this.expectedType = type; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet new file mode 100644 index 000000000..95f52f2fd --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Part|Description|Foo + +{{#requestParts}} +|{{name}} +|{{description}} +|{{foo}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet new file mode 100644 index 000000000..ca0234649 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Part|Optional|Description + +{{#requestParts}} +|{{name}} +|{{optional}} +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet new file mode 100644 index 000000000..7e6731826 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Part|Description + +{{#requestParts}} +|{{name}} +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet new file mode 100644 index 000000000..b81a83961 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet @@ -0,0 +1,5 @@ +Part | Description | Foo +---- | ----------- | --- +{{#requestParts}} +{{name}} | {{description}} | {{foo}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet new file mode 100644 index 000000000..c92cdc893 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet @@ -0,0 +1,5 @@ +Part | Optional | Description +---- | -------- | ----------- +{{#requestParts}} +{{name}} | {{optional}} | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet new file mode 100644 index 000000000..0dc1b3e9f --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Part | Description +---- | ----------- +{{#requestParts}} +{{name}} | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 28b76962b..5068d34fc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -71,8 +71,10 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; @@ -81,6 +83,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -259,6 +262,20 @@ public void requestFieldsSnippet() throws Exception { "request-fields.adoc"); } + @Test + public void requestPartsSnippet() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(fileUpload("/upload").file("foo", "bar".getBytes())) + .andExpect(status().isOk()).andDo(document("request-parts", requestParts( + partWithName("foo").description("The description")))); + + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/request-parts"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); + } + @Test public void responseFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -432,6 +449,15 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } + @Test + public void multiPart() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(fileUpload("/upload").file("test", "content".getBytes())) + .andExpect(status().isOk()).andDo(document("upload", + requestParts(partWithName("test").description("Foo")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); @@ -473,6 +499,11 @@ public String bar() { return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; } + @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public void upload() { + + } + } } diff --git a/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet b/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet new file mode 100644 index 000000000..e1db090ba --- /dev/null +++ b/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,9 @@ +|=== +|Request part|Description + +{{#requestParts}} +|`{{name}}` +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 408d29059..658793ee7 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -64,8 +64,10 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; @@ -185,6 +187,17 @@ public void requestFieldsSnippet() throws Exception { "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); } + @Test + public void requestPartsSnippet() throws Exception { + given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-parts", + requestParts(partWithName("a").description("The description")))) + .multiPart("a", "foo").post("/upload").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/request-parts"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); + } + @Test public void responseFieldsSnippet() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) @@ -343,6 +356,11 @@ public String bar() { return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; } + @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public void upload() { + + } + } } From b81c03646fd594e45d97ece21ca299056aabfb18 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 May 2016 17:15:01 +0100 Subject: [PATCH 0276/1059] Polish MustacheTemplate's javadoc --- .../restdocs/templates/mustache/MustacheTemplate.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index 9b359edbd..cb98ab2a2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -31,9 +31,7 @@ public class MustacheTemplate implements Template { private final org.springframework.restdocs.mustache.Template delegate; /** - * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. When - * rendered, the given {@code defaultContext} will be combined with the render context - * prior to executing the delegate. + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. * * @param delegate The delegate to adapt */ From 1c04ddc7e53c14943544800e9b3a11284ee578f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 15:38:49 +0100 Subject: [PATCH 0277/1059] Update contributing section, add link to Gitter chat room Closes gh-238 --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1488ceb0..18fd83473 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,12 @@ You will need Java 7 or later to build Spring REST Docs. It is built using [Grad ## Contributing Contributors to this project agree to uphold its [code of conduct][11]. -[Pull requests][12] are welcome. Please see the [contributor guidelines][13] for details. + +There are several that you can contribute to Spring REST Docs: + + - Open a [pull request][12]. Please see the [contributor guidelines][13] for details. + - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag. + - Chat with fellow users [on Gitter][16]. ## Licence @@ -46,4 +51,5 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [12]: https://help.github.com/articles/using-pull-requests/ [13]: CONTRIBUTING.md [14]: http://www.apache.org/licenses/LICENSE-2.0.html - +[15]: http://stackoverflow.com/tags/spring-restdocs +[16]: https://gitter.im/spring-projects/spring-restdocs \ No newline at end of file From 117e8ec096b5e95b575bc8258245602a6145680a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 15:48:50 +0100 Subject: [PATCH 0278/1059] Provide an API for configuring additional default snippets Previously, the default snippets could only be configured by replacing those that are configured out of the box. This commit adds a method, withAdditionalDefaults, that can be used to add one or more snippets to those that are configured out of the box. Closes gh-237 --- .../restdocs/config/SnippetConfigurer.java | 23 +++++++++++++++---- .../RestDocumentationConfigurerTests.java | 22 +++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index cb72f152c..d6475aadc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.config; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -38,9 +39,9 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList(CliDocumentation.curlRequest(), - CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse()); + private List defaultSnippets = new ArrayList<>(Arrays.asList( + CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); /** * The default encoding for documentation snippets. @@ -97,10 +98,24 @@ public TYPE withEncoding(String encoding) { * * @param defaultSnippets the default snippets * @return {@code this} + * @see #withAdditionalDefaults(Snippet...) */ @SuppressWarnings("unchecked") public TYPE withDefaults(Snippet... defaultSnippets) { - this.defaultSnippets = Arrays.asList(defaultSnippets); + this.defaultSnippets = new ArrayList<>(Arrays.asList(defaultSnippets)); + return (TYPE) this; + } + + /** + * Configures additional documentation snippets that will be produced by default. + * + * @param additionalDefaultSnippets the additional default snippets + * @return {@code this} + * @see #withDefaults(Snippet...) + */ + @SuppressWarnings("unchecked") + public TYPE withAdditionalDefaults(Snippet... additionalDefaultSnippets) { + this.defaultSnippets.addAll(Arrays.asList(additionalDefaultSnippets)); return (TYPE) this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index a629283f2..0872c20df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -119,6 +119,26 @@ public void customDefaultSnippets() { assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); } + @SuppressWarnings("unchecked") + @Test + public void additionalDefaultSnippets() { + Map configuration = new HashMap<>(); + Snippet snippet = mock(Snippet.class); + this.configurer.snippets().withAdditionalDefaults(snippet).apply(configuration, + createContext()); + assertThat(configuration, + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), + instanceOf(List.class))); + List defaultSnippets = (List) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(defaultSnippets, + contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpieRequestSnippet.class), + instanceOf(HttpRequestSnippet.class), + instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + } + @Test public void customSnippetEncoding() { Map configuration = new HashMap<>(); From 65e55ac15cf01a60bb07ef04bbab490d1f7a6b33 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 16:10:47 +0100 Subject: [PATCH 0279/1059] Allow descriptors to be added using a List in addition to varags Closes gh-242 --- .../headers/RequestHeadersSnippet.java | 19 ++++++++--- .../headers/ResponseHeadersSnippet.java | 18 ++++++++-- .../restdocs/hypermedia/LinksSnippet.java | 19 ++++++++--- .../payload/RequestFieldsSnippet.java | 34 +++++++++++++++++-- .../payload/ResponseFieldsSnippet.java | 34 +++++++++++++++++-- .../request/PathParametersSnippet.java | 20 ++++++++--- .../request/RequestParametersSnippet.java | 17 ++++++++-- .../restdocs/request/RequestPartsSnippet.java | 20 ++++++++--- 8 files changed, 154 insertions(+), 27 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index ecdc933a6..71ec61ab1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -70,10 +70,21 @@ protected Set extractActualHeaders(Operation operation) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.getHeaderDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestHeadersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.getHeaderDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index 1a4171679..8b9ec45c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -71,9 +71,21 @@ protected Set extractActualHeaders(Operation operation) { * @return the new snippet */ public final ResponseHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.getHeaderDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code ResponseHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseHeadersSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.getHeaderDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); return new ResponseHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 51b28f927..f0a4cb30e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -249,10 +249,21 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public LinksSnippet and(LinkDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.descriptorsByRel.values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final LinksSnippet and(LinkDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link + * extractor and attributes, and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final LinksSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.descriptorsByRel.values()); + combinedDescriptors.addAll(additionalDescriptors); return new LinksSnippet(this.linkExtractor, combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index cfb2ee265..3d03d71a1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -107,7 +107,19 @@ protected byte[] getContent(Operation operation) throws IOException { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + public final RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestFieldsSnippet and(List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -121,7 +133,7 @@ public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestFieldsSnippet andWithPrefix(String pathPrefix, + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); @@ -130,4 +142,22 @@ public RequestFieldsSnippet andWithPrefix(String pathPrefix, return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 06b5ac655..f3ce35392 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -108,7 +108,19 @@ protected byte[] getContent(Operation operation) throws IOException { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + public final ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseFieldsSnippet and(List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -122,7 +134,7 @@ public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public ResponseFieldsSnippet andWithPrefix(String pathPrefix, + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); @@ -131,4 +143,22 @@ public ResponseFieldsSnippet andWithPrefix(String pathPrefix, return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 509a604b7..afbd0b527 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -165,10 +165,22 @@ protected void verificationFailed(Set undocumentedParameters, * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(getParameterDescriptors().values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code PathParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final PathParametersSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); return new PathParametersSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 37a5696d6..1fa21bf7f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -129,9 +129,20 @@ protected Set extractActualParameters(Operation operation) { * @return the new snippet */ public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(getParameterDescriptors().values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestParametersSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index 1a5d30597..c55bca4e2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -115,14 +115,24 @@ protected RequestPartsSnippet(List descriptors, /** * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes * and its descriptors combined with the given {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.descriptorsByName.values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes + * and its descriptors combined with the given {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartsSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.descriptorsByName.values()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestPartsSnippet(combinedDescriptors, this.getAttributes()); } From 404b99b0a4ea4dc1908b5d7d709db9e6e5a1f2b2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 10:50:32 +0100 Subject: [PATCH 0280/1059] Avoid problems caused by | characters in table cell content Previously, a | character in a cell of an Asciidoctor table would break that table's formatting as it would be interpretted as a delimiter for the cell. The problem also affects Markdown tables where the | character is not within backticks. This commit addresses the problem with Asciidoctor tables by introducing a JMustache Lambda that escapes | characters by prefixing them with a backslash. Unfortunately, a complete solution to the problem when using Markdown is not as straightforward. Markdown only requires a | character to be espaced when it is not within backticks. Determing this isn't straightforward as the text would have to be parsed, taking into account the possibility of backticks themselves being escaped. Instead, this commit attempts to avoid the problem in a different way. The most likely source of a | character is when documenting a `@Pattern` constraint that uses a | character in its regular expression. To improve the readability of the regular expression this commit wraps it in backticks, thereby formatting it in a monospaced font in both Asciidoctor and Markdown and also avoiding any escaping problems with the latter. Closes gh-232 See gh-230 --- spring-restdocs-core/build.gradle | 1 + .../config/RestDocumentationConfigurer.java | 13 ++++- .../AsciidoctorTableCellContentLambda.java | 45 +++++++++++++++ .../templates/mustache/MustacheTemplate.java | 23 +++++++- .../mustache/MustacheTemplateEngine.java | 30 +++++++++- .../DefaultConstraintDescriptions.properties | 2 +- .../asciidoctor/default-links.snippet | 4 +- .../default-path-parameters.snippet | 4 +- .../default-request-fields.snippet | 6 +- .../default-request-headers.snippet | 4 +- .../default-request-parameters.snippet | 4 +- .../asciidoctor/default-request-parts.snippet | 4 +- .../default-response-fields.snippet | 6 +- .../default-response-headers.snippet | 4 +- .../RestDocumentationConfigurerTests.java | 30 ++++++++++ .../ConstraintDescriptionsTests.java | 2 +- ...dleConstraintDescriptionResolverTests.java | 2 +- .../headers/RequestHeadersSnippetTests.java | 20 +++++++ .../headers/ResponseHeadersSnippetTests.java | 19 +++++++ .../hypermedia/LinksSnippetTests.java | 19 +++++++ .../payload/RequestFieldsSnippetTests.java | 22 +++++++ .../payload/ResponseFieldsSnippetTests.java | 21 +++++++ .../request/PathParametersSnippetTests.java | 32 ++++++++++- .../RequestParametersSnippetTests.java | 20 +++++++ .../request/RequestPartsSnippetTests.java | 20 +++++++ ...sciidoctorTableCellContentLambdaTests.java | 57 +++++++++++++++++++ .../restdocs/test/OperationBuilder.java | 8 ++- 27 files changed, 393 insertions(+), 29 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 9f0934756..01ef4b9cc 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -38,6 +38,7 @@ dependencies { testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.hibernate:hibernate-validator' + testCompile 'org.springframework:spring-test' testRuntime 'org.glassfish:javax.el:3.0.0' } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index f77c7aa45..2b1216f4a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -17,15 +17,19 @@ package org.springframework.restdocs.config; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -102,9 +106,16 @@ public void apply(Map configuration, if (engineToUse == null) { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); + Map templateContext = new HashMap<>(); + if (snippetConfiguration.getTemplateFormat().getId() + .equals(TemplateFormats.asciidoctor().getId())) { + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); + } engineToUse = new MustacheTemplateEngine( new StandardTemplateResourceResolver( - snippetConfiguration.getTemplateFormat())); + snippetConfiguration.getTemplateFormat()), + Mustache.compiler().escapeHTML(false), templateContext); } configuration.put(TemplateEngine.class.getName(), engineToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java new file mode 100644 index 000000000..7881c6e08 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.templates.mustache; + +import java.io.IOException; +import java.io.Writer; + +import org.springframework.restdocs.mustache.Mustache.Lambda; +import org.springframework.restdocs.mustache.Template.Fragment; + +/** + * A {@link Lambda} that escapes {@code |} characters so that the do not break the table's + * formatting. + * + * @author Andy Wilkinson + */ +public final class AsciidoctorTableCellContentLambda implements Lambda { + + @Override + public void execute(Fragment fragment, Writer writer) throws IOException { + String output = fragment.execute(); + for (int i = 0; i < output.length(); i++) { + char current = output.charAt(i); + if (current == '|' && (i == 0 || output.charAt(i - 1) != '\\')) { + writer.append('\\'); + } + writer.append(current); + } + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index cb98ab2a2..1d315360a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.templates.mustache; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.springframework.restdocs.templates.Template; @@ -30,18 +32,37 @@ public class MustacheTemplate implements Template { private final org.springframework.restdocs.mustache.Template delegate; + private final Map context; + /** * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. * * @param delegate The delegate to adapt */ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { + this(delegate, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. + * During rendering, the given {@code context} and the context passed into + * {@link #render(Map)} will be combined and then passed to the delegate when it is + * {@link org.springframework.restdocs.mustache.Template#execute executed}. + * + * @param delegate The delegate to adapt + * @param context The context + */ + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate, + Map context) { this.delegate = delegate; + this.context = context; } @Override public String render(Map context) { - return this.delegate.execute(context); + Map combinedContext = new HashMap<>(this.context); + combinedContext.putAll(context); + return this.delegate.execute(combinedContext); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index 8079060ad..fadce5ae2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Map; import org.springframework.core.io.Resource; import org.springframework.restdocs.mustache.Mustache; @@ -36,9 +38,11 @@ */ public class MustacheTemplateEngine implements TemplateEngine { + private final TemplateResourceResolver templateResourceResolver; + private final Compiler compiler; - private final TemplateResourceResolver templateResourceResolver; + private final Map context; /** * Creates a new {@code MustacheTemplateEngine} that will use the given @@ -60,16 +64,36 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Compiler compiler) { + this(templateResourceResolver, compiler, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve templates and the given + * {@code compiler} to compile them. Compiled templates will be created with the given + * {@code context}. + * + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + * @param context the context to pass to compiled templates + * @see MustacheTemplate#MustacheTemplate(org.springframework.restdocs.mustache.Template, + * Map) + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + Compiler compiler, Map context) { this.templateResourceResolver = templateResourceResolver; this.compiler = compiler; + this.context = context; } @Override public Template compileTemplate(String name) throws IOException { Resource templateResource = this.templateResourceResolver .resolveTemplateResource(name); - return new MustacheTemplate(this.compiler - .compile(new InputStreamReader(templateResource.getInputStream()))); + return new MustacheTemplate( + this.compiler.compile( + new InputStreamReader(templateResource.getInputStream())), + this.context); } /** diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index 1cd5320f8..d42b5ae68 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -9,7 +9,7 @@ javax.validation.constraints.Min.description=Must be at least ${value} javax.validation.constraints.NotNull.description=Must not be null javax.validation.constraints.Null.description=Must be null javax.validation.constraints.Past.description=Must be in the past -javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' +javax.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index 132643d8e..ab50823d7 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,8 +2,8 @@ |Relation|Description {{#links}} -|`{{rel}}` -|{{description}} +|{{#tableCellContent}}`{{rel}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/links}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index a6cc9f8d4..c318e4019 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -3,8 +3,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 41e763e74..46cd43fe4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index f3d660355..790a81bce 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index f338b345f..411c33c54 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,8 +2,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet index 3ed4773b5..06a65c1e2 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -2,8 +2,8 @@ |Part|Description {{#requestParts}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/requestParts}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 41e763e74..46cd43fe4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index f3d660355..790a81bce 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0872c20df..995a6ffb6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -36,7 +36,9 @@ import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.test.util.ReflectionTestUtils; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -164,6 +166,34 @@ public void customTemplateFormat() { is(equalTo(TemplateFormats.markdown()))); } + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext, hasEntry(equalTo("tableCellContent"), + instanceOf(AsciidoctorTableCellContentLambda.class))); + } + + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown()); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext.size(), equalTo(0)); + } + private RestDocumentationContext createContext() { ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( "build"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index a3dbd9abe..34e916c56 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -118,7 +118,7 @@ public void past() { @Test public void pattern() { assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), - contains("Must match the regular expression '[A-Z][a-z]+'")); + contains("Must match the regular expression `[A-Z][a-z]+`")); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index f9a7f734e..ece62ab92 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -137,7 +137,7 @@ public void defaultMessagePast() { @Test public void defaultMessagePattern() { assertThat(constraintDescriptionForField("pattern"), - is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); + is(equalTo("Must match the regular expression `[A-Z][a-z]+`"))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 7a0c9edd0..5766661c5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -165,4 +166,23 @@ public void additionalDescriptors() throws IOException { .header("Connection", "keep-alive").build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("request-with-escaped-headers") + .request("http://localhost").header("Foo|Bar", "baz") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index d7bf553ac..417259cba 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -155,4 +156,22 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("response-with-escaped-headers") + .response().header("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 46f73b303..eb14248b7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -171,4 +172,22 @@ public void additionalDescriptors() throws IOException { .and(new LinkDescriptor("b").description("two")) .document(operationBuilder("additional-descriptors").build()); } + + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectLinks("links-with-escaped-content") + .withContents(tableWithHeader("Relation", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), + Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) + .document(operationBuilder("links-with-escaped-content").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index eac0b8438..fa23f3467 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -227,4 +228,25 @@ public void prefixedAdditionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void requestWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectRequestFields("request-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("request-fields-with-escaped-content") + .request("http://localhost").content("{\"Foo|Bar\": 5}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 574899a09..db13d9424 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -283,4 +284,24 @@ public void prefixedAdditionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void responseWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectResponseFields("response-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("response-fields-with-escaped-content") + .response().content("{\"Foo|Bar\": 5}").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c5f700654..e09809918 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -192,9 +192,37 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void pathParametersWithEscapedContent() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-escaped-content") + .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", + "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + + RequestDocumentation + .pathParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("path-parameters-with-escaped-content") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "{Foo|Bar}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + private String getTitle() { - return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" - : "`/{a}/{b}`"; + return getTitle("/{a}/{b}"); + } + + private String getTitle(String title) { + if (this.templateFormat.equals(TemplateFormats.asciidoctor())) { + return title; + } + return "`" + title + "`"; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 94135a3bf..78dac57dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -194,4 +195,23 @@ public void additionalDescriptors() throws IOException { .param("b", "bravo").build()); } + @Test + public void requestParametersWithEscapedContent() throws IOException { + this.snippet.expectRequestParameters("request-parameters-with-escaped-content") + .withContents(tableWithHeader("Parameter", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation + .requestParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parameters-with-escaped-content") + .request("http://localhost").param("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 6ce2a09a2..6468d80b6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -180,4 +181,23 @@ public void additionalDescriptors() throws IOException { .part("b", "bravo".getBytes()).build()); } + @Test + public void requestPartsWithEscapedContent() throws IOException { + this.snippet.expectRequestParts("request-parts-with-escaped-content") + .withContents(tableWithHeader("Part", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parts-with-escaped-content") + .request("http://localhost").part("Foo|Bar", "baz".getBytes()) + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java new file mode 100644 index 000000000..c9420919c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2016 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.templates.mustache; + +import java.io.IOException; +import java.io.StringWriter; + +import org.junit.Test; + +import org.springframework.restdocs.mustache.Template.Fragment; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AsciidoctorTableCellContentLambda}. + * + * @author Andy Wilkinson + */ +public class AsciidoctorTableCellContentLambdaTests { + + @Test + public void verticalBarCharactersAreEscaped() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("|foo|bar|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + + @Test + public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("\\|foo|bar\\|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 86b426a30..4e292b2eb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -29,6 +29,7 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; @@ -45,6 +46,7 @@ import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -93,9 +95,13 @@ public OperationBuilder attribute(String name, Object value) { public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { + Map templateContext = new HashMap<>(); + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine( - new StandardTemplateResourceResolver(this.templateFormat))); + new StandardTemplateResourceResolver(this.templateFormat), + Mustache.compiler().escapeHTML(false), templateContext)); } RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context); From 7f02cdf72531e673d9ce425838b412adbe463dcf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 11:04:30 +0100 Subject: [PATCH 0281/1059] Upgrade to JMustache 1.12 Closes gh-246 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e08c78a52..02b0977a5 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ subprojects { dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' dependency 'com.jayway.restassured:rest-assured:2.8.0' - dependency 'com.samskivert:jmustache:1.11' + dependency 'com.samskivert:jmustache:1.12' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' From ad549ae7c0301ea39c6e67dcbd734e943e4f90cc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 11:30:49 +0100 Subject: [PATCH 0282/1059] Ensure that additional snippets are only called once Previously, when additional snippets were added to a RestDocumentationResultHandler, those snippets would then be called for every subsequent call to handle(MvcResult). This is problematic as the snippets and their configuration are specific to a particular MvcResult that is being documented. This commit updates RestDocumentationResultHandler so that it clears its additional snippets each time handle(MvcResult) is called. Closes gh-243 --- .../RestDocumentationResultHandler.java | 7 +- .../RestDocumentationResultHandlerTests.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 962f12b47..1969e5e94 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -44,6 +44,8 @@ */ public class RestDocumentationResultHandler implements ResultHandler { + private final List additionalSnippets; + private final String identifier; private final OperationRequestPreprocessor requestPreprocessor; @@ -80,6 +82,7 @@ public class RestDocumentationResultHandler implements ResultHandler { this.requestPreprocessor = requestPreprocessor; this.responsePreprocessor = responsePreprocessor; this.snippets = new ArrayList<>(Arrays.asList(snippets)); + this.additionalSnippets = new ArrayList<>(); } @Override @@ -110,7 +113,7 @@ public void handle(MvcResult result) throws Exception { * @return this {@code ResultDocumentationResultHandler} */ public RestDocumentationResultHandler snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.additionalSnippets.addAll(Arrays.asList(snippets)); return this; } @@ -120,6 +123,8 @@ private List getSnippets(MvcResult result) { (List) result.getRequest().getAttribute( "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); + combinedSnippets.addAll(this.additionalSnippets); + this.additionalSnippets.clear(); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java new file mode 100644 index 000000000..4c8b53ded --- /dev/null +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014-2016 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.mockmvc; + +import java.util.Arrays; + +import org.junit.Test; +import org.mockito.Matchers; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.test.web.servlet.MvcResult; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestDocumentationResultHandler}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationResultHandlerTests { + + @Test + public void additionalSnippetsAreOnlyCalledOnce() throws Exception { + Snippet snippet = mock(Snippet.class); + RestDocumentationResultHandler resultHandler = new RestDocumentationResultHandler( + "test", snippet); + Snippet defaultSnippet = mock(Snippet.class); + Snippet additionalSnippet = mock(Snippet.class); + resultHandler.snippets(additionalSnippet); + MvcResult result = mock(MvcResult.class); + MockHttpServletRequest request = new MockHttpServletRequest("GET", + "http://localhost"); + request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", + Arrays.asList(defaultSnippet)); + given(result.getRequest()).willReturn(request); + given(result.getResponse()).willReturn(new MockHttpServletResponse()); + resultHandler.handle(result); + resultHandler.handle(result); + verify(defaultSnippet, times(2)).document(Matchers.any(Operation.class)); + verify(snippet, times(2)).document(Matchers.any(Operation.class)); + verify(additionalSnippet, times(1)).document(Matchers.any(Operation.class)); + } + +} From 09c9cac857e07bf33c6894bfaff02fb07f45dc70 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 21 May 2016 12:34:23 +0900 Subject: [PATCH 0283/1059] Polish --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/test/java/com/example/mockmvc/Payload.java | 2 +- .../test/java/com/example/restassured/Payload.java | 11 +++++------ .../preprocess/OperationPreprocessorAdapter.java | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index eb9c1fe5f..094c64c29 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -171,7 +171,7 @@ static content] by Spring Boot. To do so, configure your project's build so that 1. The documentation is generated before the jar is built 2. The generated documentation is included in the jar -[source,xml,indent=0,role="primary",role="primary"] +[source,xml,indent=0,role="primary"] .Maven ---- <1> @@ -253,7 +253,7 @@ When using JUnit, the first step in generating documentation snippets is to decl snippets should be written. This output directory should match the snippets directory that you have configured in your `build.gradle` or `pom.xml` file. -For Maven (`pom.xml` that will typically be `target/generated-snippets` and for +For Maven (`pom.xml`) that will typically be `target/generated-snippets` and for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: [source,java,indent=0,role="primary"] diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index b811c1a6c..615354a3f 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -80,7 +80,7 @@ public void descriptorReuse() throws Exception { this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(document("book", responseFields(book))); // <1> - // end::single-book[] + // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 225eb6194..155e357e5 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -68,9 +68,9 @@ public void constraints() throws Exception { fieldWithPath("email").description("The user's email address") .attributes(key("constraints") .value("Must be a valid email address"))))) // <3> - // end::constraints[] - .when().post("/users") - .then().assertThat().statusCode(is(200)); + // end::constraints[] + .when().post("/users") + .then().assertThat().statusCode(is(200)); } public void descriptorReuse() throws Exception { @@ -90,10 +90,9 @@ public void descriptorReuse() throws Exception { .filter(document("books", responseFields( fieldWithPath("[]").description("An array of books")) // <1> .andWithPrefix("[].", book))) // <2> - .when().get("/books/1") - .then().assertThat().statusCode(is(200)); + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); // end::book-array[] - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java index f2654c1bf..544de0f45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -21,7 +21,7 @@ /** * An implementation of {@link OperationPreprocessor} that returns the request and - * response as-is. To be subclasses by preprocessor implementations that only modify the + * response as-is. To be subclassed by preprocessor implementations that only modify the * request or the response. * * @author Andy Wilkinson From d58f69175fc00dace8c999c71b0457b0c0e67367 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Mon, 23 May 2016 20:37:12 +0200 Subject: [PATCH 0284/1059] Provide utility method for applying a path prefix to field descriptors applyPathPrefix is generally useful but, previously, it was only available as a protected method on AbstractFieldsSnippet. This commit moves the method from AbstractFieldsSnippet to PayloadDocumentation where it's now public and static. Closes gh-245 --- .../payload/AbstractFieldsSnippet.java | 26 ----------------- .../payload/PayloadDocumentation.java | 29 +++++++++++++++++++ .../payload/RequestFieldsSnippet.java | 5 ++-- .../payload/ResponseFieldsSnippet.java | 5 ++-- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index c64507128..fcc67c360 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -204,30 +204,4 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } - /** - * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} - * applied to their paths. - * - * @param pathPrefix the path prefix - * @param descriptors the descriptors to copy - * @return the copied descriptors with the prefix applied - */ - protected final List applyPathPrefix(String pathPrefix, - List descriptors) { - List prefixedDescriptors = new ArrayList<>(); - for (FieldDescriptor descriptor : descriptors) { - FieldDescriptor prefixedDescriptor = new FieldDescriptor( - pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()); - if (descriptor.isIgnored()) { - prefixedDescriptor.ignored(); - } - if (descriptor.isOptional()) { - prefixedDescriptor.optional(); - } - prefixedDescriptors.add(prefixedDescriptor); - } - return prefixedDescriptors; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bb94c960d..bfce18790 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -16,7 +16,9 @@ package org.springframework.restdocs.payload; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -264,4 +266,31 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + public static List applyPathPrefix(String pathPrefix, + List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + FieldDescriptor prefixedDescriptor = new FieldDescriptor( + pathPrefix + descriptor.getPath()) + .description(descriptor.getDescription()) + .type(descriptor.getType()); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 3d03d71a1..ea8f68b2c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -138,7 +138,7 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); combinedDescriptors.addAll( - applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } @@ -156,7 +156,8 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); - combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index f3ce35392..cabe29484 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -139,7 +139,7 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); combinedDescriptors.addAll( - applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } @@ -157,7 +157,8 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); - combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } From 5edc27fc990b984c733d88850f9c971529846da8 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Tue, 24 May 2016 11:42:35 +0200 Subject: [PATCH 0285/1059] Allow descriptors to be provided as a List as well as via varags Closes gh-247 --- .../restdocs/headers/HeaderDocumentation.java | 69 +++++ .../hypermedia/HypermediaDocumentation.java | 195 ++++++++++++++ .../payload/PayloadDocumentation.java | 167 ++++++++++++ .../request/RequestDocumentation.java | 244 ++++++++++++++++++ 4 files changed, 675 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 1eb4f0f9e..740f6a78b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.headers; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.snippet.Snippet; @@ -59,6 +60,21 @@ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descripto return new RequestHeadersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a new {@link Snippet} that will document the headers of the API operation's + * request. The headers will be documented using the given {@code descriptors}. + *

            + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers + * @see #headerWithName(String) + */ + public static RequestHeadersSnippet requestHeaders(List descriptors) { + return new RequestHeadersSnippet(descriptors); + } + /** * Returns a new {@link Snippet} that will document the headers of the API * operations's request. The given {@code attributes} will be available during snippet @@ -77,6 +93,24 @@ public static RequestHeadersSnippet requestHeaders(Map attribute return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@link Snippet} that will document the headers of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the headers will be documented using the given {@code descriptors}. + *

            + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers + * @see #headerWithName(String) + */ + public static RequestHeadersSnippet requestHeaders(Map attributes, + List descriptors) { + return new RequestHeadersSnippet(descriptors, attributes); + } + /** * Returns a new {@link Snippet} that will document the headers of the API operation's * response. The headers will be documented using the given {@code descriptors}. @@ -93,6 +127,22 @@ public static ResponseHeadersSnippet responseHeaders( return new ResponseHeadersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a new {@link Snippet} that will document the headers of the API operation's + * response. The headers will be documented using the given {@code descriptors}. + *

            + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers + * @see #headerWithName(String) + */ + public static ResponseHeadersSnippet responseHeaders( + List descriptors) { + return new ResponseHeadersSnippet(descriptors); + } + /** * Returns a new {@link Snippet} that will document the headers of the API * operations's response. The given {@code attributes} will be available during @@ -112,4 +162,23 @@ public static ResponseHeadersSnippet responseHeaders(Map attribu return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@link Snippet} that will document the headers of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the headers will be documented using the given + * {@code descriptors}. + *

            + * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers + * @see #headerWithName(String) + */ + public static ResponseHeadersSnippet responseHeaders(Map attributes, + List descriptors) { + return new ResponseHeadersSnippet(descriptors, attributes); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 95f70b3be..a6098ce52 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.hypermedia; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -66,6 +67,31 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

            + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

            + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response automatically based on its @@ -86,6 +112,25 @@ public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { Arrays.asList(descriptors), true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -115,6 +160,34 @@ public static LinksSnippet links(Map attributes, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API call's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response automatically based on its content type + * and will be documented using the given {@code descriptors}. + *

            + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

            + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(Map attributes, + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -138,6 +211,28 @@ public static LinksSnippet relaxedLinks(Map attributes, Arrays.asList(descriptors), attributes, true); } + /** + * Returns a new {@code Snippet} that will document the links in the API call's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response automatically based on its content type + * and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(Map attributes, + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -165,6 +260,33 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

            + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

            + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(LinkExtractor linkExtractor, + List descriptors) { + return new LinksSnippet(linkExtractor, descriptors); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -186,6 +308,27 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -215,6 +358,35 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

            + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

            + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(LinkExtractor linkExtractor, + Map attributes, List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, attributes); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -239,6 +411,29 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

            + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

            + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + Map attributes, List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, attributes, true); + } + /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bfce18790..464b7e2f1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -121,6 +121,29 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) return new RequestFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet requestFields(List descriptors) { + return new RequestFieldsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -137,6 +160,22 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + List descriptors) { + return new RequestFieldsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -163,6 +202,32 @@ public static RequestFieldsSnippet requestFields(Map attributes, return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet requestFields(Map attributes, + List descriptors) { + return new RequestFieldsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -181,6 +246,24 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + Map attributes, List descriptors) { + return new RequestFieldsSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -205,6 +288,30 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(List descriptors) { + return new ResponseFieldsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -222,6 +329,23 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + List descriptors) { + return new ResponseFieldsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -248,6 +372,32 @@ public static ResponseFieldsSnippet responseFields(Map attribute return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(Map attributes, + List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -266,6 +416,23 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes, true); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 4fec764af..c343cd7c7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.request; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.operation.OperationRequest; @@ -76,6 +77,28 @@ public static PathParametersSnippet pathParameters( return new PathParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. + *

            + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet pathParameters( + List descriptors) { + return new PathParametersSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The parameters will be documented using the given @@ -92,6 +115,22 @@ public static PathParametersSnippet relaxedPathParameters( return new PathParametersSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + List descriptors) { + return new PathParametersSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -116,6 +155,30 @@ public static PathParametersSnippet pathParameters(Map attribute return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. + *

            + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet pathParameters(Map attributes, + List descriptors) { + return new PathParametersSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -134,6 +197,24 @@ public static PathParametersSnippet relaxedPathParameters( return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + Map attributes, List descriptors) { + return new PathParametersSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -157,6 +238,29 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

            + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet requestParameters( + List descriptors) { + return new RequestParametersSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -174,6 +278,23 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + List descriptors) { + return new RequestParametersSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -199,6 +320,31 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

            + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet requestParameters( + Map attributes, List descriptors) { + return new RequestParametersSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -218,6 +364,25 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

            + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + Map attributes, List descriptors) { + return new RequestParametersSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The parts will be documented using the given {@code descriptors}. @@ -239,6 +404,27 @@ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descript return new RequestPartsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parts will be documented using the given {@code descriptors}. + *

            + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

            + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(List descriptors) { + return new RequestPartsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The parameters will be documented using the given {@code descriptors}. @@ -255,6 +441,22 @@ public static RequestPartsSnippet relaxedRequestParts( return new RequestPartsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parameters will be documented using the given {@code descriptors}. + *

            + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet relaxedRequestParts( + List descriptors) { + return new RequestPartsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The given {@code attributes} will be available during snippet rendering @@ -279,6 +481,30 @@ public static RequestPartsSnippet requestParts(Map attributes, return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

            + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

            + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(Map attributes, + List descriptors) { + return new RequestPartsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The given {@code attributes} will be available during snippet rendering @@ -297,4 +523,22 @@ public static RequestPartsSnippet relaxedRequestParts(Map attrib return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

            + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestPartsSnippet relaxedRequestParts(Map attributes, + List descriptors) { + return new RequestPartsSnippet(descriptors, attributes, true); + } + } From 81b2f11ebfa223cdb2b27dd1ea39cabdbecd9a1a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 13:54:17 +0100 Subject: [PATCH 0286/1059] Polish "Allow descriptors to be provided as a List as well as via varags" See gh-247 --- .../restdocs/headers/HeaderDocumentation.java | 12 ++++--- .../hypermedia/HypermediaDocumentation.java | 31 +++++++++---------- .../payload/PayloadDocumentation.java | 27 +++++++++------- .../request/RequestDocumentation.java | 28 +++++++++-------- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 740f6a78b..9f32638e1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -27,6 +27,7 @@ * * @author Andreas Evers * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class HeaderDocumentation { @@ -57,7 +58,7 @@ public static HeaderDescriptor headerWithName(String name) { * @see #headerWithName(String) */ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descriptors) { - return new RequestHeadersSnippet(Arrays.asList(descriptors)); + return requestHeaders(Arrays.asList(descriptors)); } /** @@ -71,7 +72,8 @@ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descripto * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static RequestHeadersSnippet requestHeaders(List descriptors) { + public static RequestHeadersSnippet requestHeaders( + List descriptors) { return new RequestHeadersSnippet(descriptors); } @@ -90,7 +92,7 @@ public static RequestHeadersSnippet requestHeaders(List descri */ public static RequestHeadersSnippet requestHeaders(Map attributes, HeaderDescriptor... descriptors) { - return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); + return requestHeaders(attributes, Arrays.asList(descriptors)); } /** @@ -124,7 +126,7 @@ public static RequestHeadersSnippet requestHeaders(Map attribute */ public static ResponseHeadersSnippet responseHeaders( HeaderDescriptor... descriptors) { - return new ResponseHeadersSnippet(Arrays.asList(descriptors)); + return responseHeaders(Arrays.asList(descriptors)); } /** @@ -159,7 +161,7 @@ public static ResponseHeadersSnippet responseHeaders( */ public static ResponseHeadersSnippet responseHeaders(Map attributes, HeaderDescriptor... descriptors) { - return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); + return responseHeaders(attributes, Arrays.asList(descriptors)); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index a6098ce52..191ffea35 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -24,6 +24,7 @@ * Static factory methods for documenting a RESTful API that utilizes Hypermedia. * * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class HypermediaDocumentation { @@ -63,8 +64,7 @@ public static LinkDescriptor linkWithRel(String rel) { * @return the snippet that will document the links */ public static LinksSnippet links(LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors)); + return links(Arrays.asList(descriptors)); } /** @@ -108,8 +108,7 @@ public static LinksSnippet links(List descriptors) { * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), true); + return relaxedLinks(Arrays.asList(descriptors)); } /** @@ -156,8 +155,7 @@ public static LinksSnippet relaxedLinks(List descriptors) { */ public static LinksSnippet links(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), attributes); + return links(attributes, Arrays.asList(descriptors)); } /** @@ -207,8 +205,7 @@ public static LinksSnippet links(Map attributes, */ public static LinksSnippet relaxedLinks(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), attributes, true); + return relaxedLinks(attributes, Arrays.asList(descriptors)); } /** @@ -229,8 +226,9 @@ public static LinksSnippet relaxedLinks(Map attributes, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(Map attributes, - List descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, true); + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, + true); } /** @@ -257,7 +255,7 @@ public static LinksSnippet relaxedLinks(Map attributes, */ public static LinksSnippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); + return links(linkExtractor, Arrays.asList(descriptors)); } /** @@ -305,7 +303,7 @@ public static LinksSnippet links(LinkExtractor linkExtractor, */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); + return relaxedLinks(linkExtractor, Arrays.asList(descriptors)); } /** @@ -325,7 +323,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - List descriptors) { + List descriptors) { return new LinksSnippet(linkExtractor, descriptors, true); } @@ -355,7 +353,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, */ public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); + return links(linkExtractor, attributes, Arrays.asList(descriptors)); } /** @@ -407,8 +405,7 @@ public static LinksSnippet links(LinkExtractor linkExtractor, */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes, - true); + return relaxedLinks(linkExtractor, attributes, Arrays.asList(descriptors)); } /** @@ -430,7 +427,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - Map attributes, List descriptors) { + Map attributes, List descriptors) { return new LinksSnippet(linkExtractor, descriptors, attributes, true); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 464b7e2f1..9a552c92e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -26,6 +26,7 @@ * * @author Andreas Evers * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class PayloadDocumentation { @@ -118,7 +119,7 @@ public static FieldDescriptor fieldWithPath(String path) { * @see #fieldWithPath(String) */ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors)); + return requestFields(Arrays.asList(descriptors)); } /** @@ -157,7 +158,7 @@ public static RequestFieldsSnippet requestFields(List descripto */ public static RequestFieldsSnippet relaxedRequestFields( FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), true); + return relaxedRequestFields(Arrays.asList(descriptors)); } /** @@ -199,7 +200,7 @@ public static RequestFieldsSnippet relaxedRequestFields( */ public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); + return requestFields(attributes, Arrays.asList(descriptors)); } /** @@ -243,7 +244,7 @@ public static RequestFieldsSnippet requestFields(Map attributes, */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestFields(attributes, Arrays.asList(descriptors)); } /** @@ -285,7 +286,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @see #fieldWithPath(String) */ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors)); + return responseFields(Arrays.asList(descriptors)); } /** @@ -308,7 +309,8 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static ResponseFieldsSnippet responseFields(List descriptors) { + public static ResponseFieldsSnippet responseFields( + List descriptors) { return new ResponseFieldsSnippet(descriptors); } @@ -326,7 +328,7 @@ public static ResponseFieldsSnippet responseFields(List descrip */ public static ResponseFieldsSnippet relaxedResponseFields( FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); + return relaxedResponseFields(Arrays.asList(descriptors)); } /** @@ -369,7 +371,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( */ public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); + return responseFields(attributes, Arrays.asList(descriptors)); } /** @@ -413,10 +415,11 @@ public static ResponseFieldsSnippet responseFields(Map attribute */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedResponseFields(attributes, Arrays.asList(descriptors)); } - /** Returns a {@code Snippet} that will document the fields of the API operation's + /** + * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} * and the given {@code attributes} will be available during snippet generation. *

            @@ -447,8 +450,8 @@ public static List applyPathPrefix(String pathPrefix, for (FieldDescriptor descriptor : descriptors) { FieldDescriptor prefixedDescriptor = new FieldDescriptor( pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()); + .description(descriptor.getDescription()) + .type(descriptor.getType()); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index c343cd7c7..043fdfe6f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -26,6 +26,7 @@ * Static factory methods for documenting aspects of a request sent to a RESTful API. * * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class RequestDocumentation { @@ -74,7 +75,7 @@ public static RequestPartDescriptor partWithName(String name) { */ public static PathParametersSnippet pathParameters( ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors)); + return pathParameters(Arrays.asList(descriptors)); } /** @@ -112,7 +113,7 @@ public static PathParametersSnippet pathParameters( */ public static PathParametersSnippet relaxedPathParameters( ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), true); + return relaxedPathParameters(Arrays.asList(descriptors)); } /** @@ -152,7 +153,7 @@ public static PathParametersSnippet relaxedPathParameters( */ public static PathParametersSnippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), attributes); + return pathParameters(attributes, Arrays.asList(descriptors)); } /** @@ -194,7 +195,7 @@ public static PathParametersSnippet pathParameters(Map attribute */ public static PathParametersSnippet relaxedPathParameters( Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedPathParameters(attributes, Arrays.asList(descriptors)); } /** @@ -235,7 +236,7 @@ public static PathParametersSnippet relaxedPathParameters( */ public static RequestParametersSnippet requestParameters( ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors)); + return requestParameters(Arrays.asList(descriptors)); } /** @@ -275,7 +276,7 @@ public static RequestParametersSnippet requestParameters( */ public static RequestParametersSnippet relaxedRequestParameters( ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), true); + return relaxedRequestParameters(Arrays.asList(descriptors)); } /** @@ -317,7 +318,7 @@ public static RequestParametersSnippet relaxedRequestParameters( */ public static RequestParametersSnippet requestParameters( Map attributes, ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); + return requestParameters(attributes, Arrays.asList(descriptors)); } /** @@ -361,7 +362,7 @@ public static RequestParametersSnippet requestParameters( */ public static RequestParametersSnippet relaxedRequestParameters( Map attributes, ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestParameters(attributes, Arrays.asList(descriptors)); } /** @@ -401,7 +402,7 @@ public static RequestParametersSnippet relaxedRequestParameters( * @see OperationRequest#getParts() */ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors)); + return requestParts(Arrays.asList(descriptors)); } /** @@ -421,7 +422,8 @@ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descript * @return the snippet * @see OperationRequest#getParts() */ - public static RequestPartsSnippet requestParts(List descriptors) { + public static RequestPartsSnippet requestParts( + List descriptors) { return new RequestPartsSnippet(descriptors); } @@ -438,7 +440,7 @@ public static RequestPartsSnippet requestParts(List descr */ public static RequestPartsSnippet relaxedRequestParts( RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), true); + return relaxedRequestParts(Arrays.asList(descriptors)); } /** @@ -478,7 +480,7 @@ public static RequestPartsSnippet relaxedRequestParts( */ public static RequestPartsSnippet requestParts(Map attributes, RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); + return requestParts(attributes, Arrays.asList(descriptors)); } /** @@ -520,7 +522,7 @@ public static RequestPartsSnippet requestParts(Map attributes, */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestParts(attributes, Arrays.asList(descriptors)); } /** From 4715351cce088d1aa121fea7ae8986361a803e1d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 May 2016 16:12:12 +0200 Subject: [PATCH 0287/1059] Make it easier to provide a placeholder resolver that uses the context Closes gh-235 --- .../config/RestDocumentationConfigurer.java | 4 +- .../snippet/PlaceholderResolverFactory.java | 38 +++++++++++++++++++ ...tionContextPlaceholderResolverFactory.java | 37 ++++++++++++++++++ .../snippet/StandardWriterResolver.java | 37 +++++++++++++----- .../snippet/StandardWriterResolverTests.java | 7 ++-- .../restdocs/test/OperationBuilder.java | 4 +- 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 2b1216f4a..c6cb1ffc4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -23,7 +23,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.mustache.Mustache; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -138,7 +138,7 @@ public void apply(Map configuration, SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context), + new RestDocumentationContextPlaceholderResolverFactory(), snippetConfiguration.getEncoding(), snippetConfiguration.getTemplateFormat()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java new file mode 100644 index 000000000..6aec68d7b --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2016 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.snippet; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * A factory for creating {@link PlaceholderResolver} instances. + * + * @author Andy Wilkinson + * @since 1.1 + */ +public interface PlaceholderResolverFactory { + + /** + * Creates a new {@link PlaceholderResolver} using the given {@code context}. + * + * @param context the context + * @return the placeholder resolver + */ + PlaceholderResolver create(RestDocumentationContext context); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java new file mode 100644 index 000000000..0ddb24562 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 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.snippet; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * A {@link PlaceholderResolverFactory} that creates + * {@link RestDocumentationContextPlaceholderResolver} instances. + * + * @author Andy Wilkinson + * @since 1.1 + */ +public final class RestDocumentationContextPlaceholderResolverFactory + implements PlaceholderResolverFactory { + + @Override + public PlaceholderResolver create(RestDocumentationContext context) { + return new RestDocumentationContextPlaceholderResolver(context); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 479407ea0..2b4144931 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -35,7 +35,7 @@ */ public final class StandardWriterResolver implements WriterResolver { - private final PlaceholderResolver placeholderResolver; + private final PlaceholderResolverFactory placeholderResolverFactory; private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "{", "}"); @@ -52,27 +52,29 @@ public final class StandardWriterResolver implements WriterResolver { * * @param placeholderResolver the placeholder resolver * @deprecated since 1.1.0 in favor of - * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, TemplateFormat)} + * {@link #StandardWriterResolver(PlaceholderResolverFactory, String, TemplateFormat)} */ @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { - this(placeholderResolver, "UTF-8", TemplateFormats.asciidoctor()); + this(new SingleInstancePlaceholderResolverFactory(placeholderResolver), "UTF-8", + TemplateFormats.asciidoctor()); } /** - * Creates a new {@code StandardWriterResolver} that will use the given - * {@code placeholderResolver} to resolve any placeholders in the + * Creates a new {@code StandardWriterResolver} that will use a + * {@link PlaceholderResolver} created from the given + * {@code placeholderResolverFactory} to resolve any placeholders in the * {@code operationName}. Writers will use the given {@code encoding} and, when * writing to a file, will use a filename appropriate for content generated from * templates in the given {@code templateFormat}. * - * @param placeholderResolver the placeholder resolver + * @param placeholderResolverFactory the placeholder resolver factory * @param encoding the encoding * @param templateFormat the snippet format */ - public StandardWriterResolver(PlaceholderResolver placeholderResolver, + public StandardWriterResolver(PlaceholderResolverFactory placeholderResolverFactory, String encoding, TemplateFormat templateFormat) { - this.placeholderResolver = placeholderResolver; + this.placeholderResolverFactory = placeholderResolverFactory; this.encoding = encoding; this.templateFormat = templateFormat; } @@ -82,7 +84,7 @@ public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile( this.propertyPlaceholderHelper.replacePlaceholders(operationName, - this.placeholderResolver), + this.placeholderResolverFactory.create(context)), snippetName + "." + this.templateFormat.getFileExtension(), context); if (outputFile != null) { @@ -127,4 +129,21 @@ private void createDirectoriesIfNecessary(File outputFile) { } } + private static final class SingleInstancePlaceholderResolverFactory + implements PlaceholderResolverFactory { + + private final PlaceholderResolver placeholderResolver; + + private SingleInstancePlaceholderResolverFactory( + PlaceholderResolver placeholderResolver) { + this.placeholderResolver = placeholderResolver; + } + + @Override + public PlaceholderResolver create(RestDocumentationContext context) { + return this.placeholderResolver; + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index af5ccd149..a4d3460d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -22,7 +22,6 @@ import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -36,11 +35,11 @@ */ public class StandardWriterResolverTests { - private final PlaceholderResolver placeholderResolver = mock( - PlaceholderResolver.class); + private final PlaceholderResolverFactory placeholderResolverFactory = mock( + PlaceholderResolverFactory.class); private final StandardWriterResolver resolver = new StandardWriterResolver( - this.placeholderResolver, "UTF-8", asciidoctor()); + this.placeholderResolverFactory, "UTF-8", asciidoctor()); @Test public void absoluteInput() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 4e292b2eb..3032a6f5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -39,7 +39,7 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -107,7 +107,7 @@ public Operation build() { this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context), "UTF-8", + new RestDocumentationContextPlaceholderResolverFactory(), "UTF-8", this.templateFormat)); return new StandardOperation(this.name, (this.requestBuilder == null From 6935f603f28e6e0d9dd8b1ec49b0b53dbfe8c1fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 16:27:03 +0100 Subject: [PATCH 0288/1059] Polish ResourceBundleConstraintDescriptionResolver and related tests Closes gh-234 --- build.gradle | 5 +- spring-restdocs-core/build.gradle | 2 +- ...ceBundleConstraintDescriptionResolver.java | 31 +++ .../ConstraintDescriptionsTests.java | 182 +++--------------- 4 files changed, 65 insertions(+), 155 deletions(-) diff --git a/build.gradle b/build.gradle index 02b0977a5..38edf1e73 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,9 @@ ext { springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', - "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", - 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/' + 'http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/', + 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/', + 'https://docs.jboss.org/hibernate/stable/validator/api/' ] as String[] } diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 01ef4b9cc..1bee13f54 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -34,10 +34,10 @@ dependencies { optional 'commons-codec:commons-codec' optional 'javax.validation:validation-api' optional 'junit:junit' + optional 'org.hibernate:hibernate-validator' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.hibernate:hibernate-validator' testCompile 'org.springframework:spring-test' testRuntime 'org.glassfish:javax.el:3.0.0' } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 759950a64..c55596c33 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -34,6 +34,19 @@ import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.EAN; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.LuhnCheck; +import org.hibernate.validator.constraints.Mod10Check; +import org.hibernate.validator.constraints.Mod11Check; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; +import org.hibernate.validator.constraints.URL; + import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -62,6 +75,24 @@ *

          • {@link Size} * * + *

            + * Default descriptions are also provided for Hibernate Validator's constraints: + * + *

              + *
            • {@link CreditCardNumber} + *
            • {@link EAN} + *
            • {@link Email} + *
            • {@link Length} + *
            • {@link LuhnCheck} + *
            • {@link Mod10Check} + *
            • {@link Mod11Check} + *
            • {@link NotBlank} + *
            • {@link NotEmpty} + *
            • {@link Range} + *
            • {@link SafeHtml} + *
            • {@link URL} + *
            + * * @author Andy Wilkinson */ public class ResourceBundleConstraintDescriptionResolver diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 34e916c56..70c0d1abc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -16,28 +16,17 @@ package org.springframework.restdocs.constraints; -import java.math.BigDecimal; -import java.util.Date; - -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Future; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; +import java.util.Arrays; +import java.util.Collections; import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link ConstraintDescriptions}. @@ -46,152 +35,41 @@ */ public class ConstraintDescriptionsTests { - private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( - Constrained.class); - - @Test - public void assertFalse() { - assertThat(this.constraintDescriptions.descriptionsForProperty("assertFalse"), - contains("Must be false")); - } + private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class); - @Test - public void assertTrue() { - assertThat(this.constraintDescriptions.descriptionsForProperty("assertTrue"), - contains("Must be true")); - } + private final ConstraintDescriptionResolver constraintDescriptionResolver = mock( + ConstraintDescriptionResolver.class); - @Test - public void decimalMax() { - assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMax"), - contains("Must be at most 9.875")); - } - - @Test - public void decimalMin() { - assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMin"), - contains("Must be at least 1.5")); - } - - @Test - public void digits() { - assertThat(this.constraintDescriptions.descriptionsForProperty("digits"), - contains("Must have at most 2 integral digits and 5 fractional digits")); - } - - @Test - public void future() { - assertThat(this.constraintDescriptions.descriptionsForProperty("future"), - contains("Must be in the future")); - } - - @Test - public void max() { - assertThat(this.constraintDescriptions.descriptionsForProperty("max"), - contains("Must be at most 10")); - } - - @Test - public void min() { - assertThat(this.constraintDescriptions.descriptionsForProperty("min"), - contains("Must be at least 5")); - } - - @Test - public void notNull() { - assertThat(this.constraintDescriptions.descriptionsForProperty("notNull"), - contains("Must not be null")); - } - - @Test - public void nul() { - assertThat(this.constraintDescriptions.descriptionsForProperty("nul"), - contains("Must be null")); - } - - @Test - public void past() { - assertThat(this.constraintDescriptions.descriptionsForProperty("past"), - contains("Must be in the past")); - } - - @Test - public void pattern() { - assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), - contains("Must match the regular expression `[A-Z][a-z]+`")); - } - - @Test - public void size() { - assertThat(this.constraintDescriptions.descriptionsForProperty("size"), - contains("Size must be between 0 and 10 inclusive")); - } - - @Test - public void sizeList() { - assertThat(this.constraintDescriptions.descriptionsForProperty("sizeList"), - contains("Size must be between 1 and 4 inclusive", - "Size must be between 8 and 10 inclusive")); - } + private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( + Constrained.class, this.constraintResolver, + this.constraintDescriptionResolver); @Test - public void unconstrained() { - assertThat(this.constraintDescriptions.descriptionsForProperty("unconstrained"), - hasSize(0)); + public void descriptionsForConstraints() { + Constraint constraint1 = new Constraint("constraint1", + Collections.emptyMap()); + Constraint constraint2 = new Constraint("constraint2", + Collections.emptyMap()); + given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) + .willReturn(Arrays.asList(constraint1, constraint2)); + given(this.constraintDescriptionResolver.resolveDescription(constraint1)) + .willReturn("Bravo"); + given(this.constraintDescriptionResolver.resolveDescription(constraint2)) + .willReturn("Alpha"); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo"), + contains("Alpha", "Bravo")); } @Test - public void nonExistentProperty() { - assertThat(this.constraintDescriptions.descriptionsForProperty("doesNotExist"), - hasSize(0)); + public void emptyListOfDescriptionsWhenThereAreNoConstraints() { + given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) + .willReturn(Collections.emptyList()); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size(), + is(equalTo(0))); } private static class Constrained { - @AssertFalse - private boolean assertFalse; - - @AssertTrue - private boolean assertTrue; - - @DecimalMax("9.875") - private BigDecimal decimalMax; - - @DecimalMin("1.5") - private BigDecimal decimalMin; - - @Digits(fraction = 5, integer = 2) - private BigDecimal digits; - - @Future - private Date future; - - @NotNull - private String notNull; - - @Max(10) - private int max; - - @Min(5) - private int min; - - @Null - private String nul; - - @Past - private Date past; - - @Pattern(regexp = "[A-Z][a-z]+") - private String pattern; - - @Size(min = 0, max = 10) - private String size; - - @Size.List({ @Size(min = 1, max = 4), @Size(min = 8, max = 10) }) - private String sizeList; - - @SuppressWarnings("unused") - private String unconstrained; } } From cb1f98c601ecedd0bcc20fb857dfbbceca3dd0b9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 16:39:57 +0100 Subject: [PATCH 0289/1059] Improve handling of parameters with null values Closes gh-199 --- .../restdocs/operation/Parameters.java | 4 + .../restdocs/operation/ParametersTests.java | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index 0235ec800..b696c2927 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.StringUtils; /** * The parameters received in a request. @@ -68,6 +69,9 @@ private static void doAppend(StringBuilder sb, String toAppend) { } private static String urlEncodeUTF8(String s) { + if (!StringUtils.hasLength(s)) { + return ""; + } try { return URLEncoder.encode(s, "UTF-8"); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java new file mode 100644 index 000000000..3c9228861 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014-2016 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.operation; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link Parameters}. + * + * @author Andy Wilkinson + */ +public class ParametersTests { + + private final Parameters parameters = new Parameters(); + + @Test + public void queryStringForNoParameters() { + assertThat(this.parameters.toQueryString(), is(equalTo(""))); + } + + @Test + public void queryStringForSingleParameter() { + this.parameters.add("a", "b"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=b"))); + } + + @Test + public void queryStringForSingleParameterWithMultipleValues() { + this.parameters.add("a", "b"); + this.parameters.add("a", "c"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=b&a=c"))); + } + + @Test + public void queryStringForMutipleParameters() { + this.parameters.add("a", "alpha"); + this.parameters.add("b", "bravo"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha&b=bravo"))); + } + + @Test + public void queryStringForParameterWithEmptyValue() { + this.parameters.add("a", ""); + assertThat(this.parameters.toQueryString(), is(equalTo("a="))); + } + + @Test + public void queryStringForParameterWithNullValue() { + this.parameters.add("a", null); + assertThat(this.parameters.toQueryString(), is(equalTo("a="))); + } + + @Test + public void queryStringForParameterThatRequiresEncoding() { + this.parameters.add("a", "alpha&bravo"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha%26bravo"))); + } + +} From 42353a0b5f6ef2c384640b51870a7adac0d53ece Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 10:46:39 +0100 Subject: [PATCH 0290/1059] Improve the API for generating additional snippets Previously, if alwaysDo was used and the user wanted to generate one or more additional snippets when calling perform a separate call to `snippets` was made prior to calling `perform`. This had two problems: - it required the result handler to be stateful (see gh-243) - it wasn't clear that the additional snippets would be produced when a subsequent call to perform was made This commit introduces a new API that allows the additional snippets to be specified within the MockMvc call. The old API has been deprecated and will be removed in 2.0. Closes gh-249 --- .../customizing-requests-and-responses.adoc | 8 +-- .../mockmvc/EveryTestPreprocessing.java | 15 +++--- .../restassured/EveryTestPreprocessing.java | 12 ++--- .../generate/RestDocumentationGenerator.java | 17 ++++++ .../RestDocumentationGeneratorTests.java | 31 +++++++++++ .../RestDocumentationResultHandler.java | 35 ++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 20 +++++++ .../restassured/RestDocumentationFilter.java | 53 ++++++++++++++++--- ...uredRestDocumentationIntegrationTests.java | 19 +++++++ 9 files changed, 186 insertions(+), 24 deletions(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 04a1b48e1..8f0ffaf0c 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -58,17 +58,17 @@ Then, in each test, any configuration specific to that test can be performed. Fo ---- include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use] ---- -<1> Document the links specific to the resource that is being tested -<2> The request and response will be preprocessed due to the use of `alwaysDo` above. +<1> The request and response will be preprocessed due to the use of `alwaysDo` above. +<2> Document the links specific to the resource that is being tested [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ---- -<1> Document the links specific to the resource that is being tested -<2> The request and response will be preprocessed due to the configuration of the +<1> The request and response will be preprocessed due to the configuration of the `RequestSpecification` in the `setUp` method. +<2> Document the links specific to the resource that is being tested Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See <> for further details. diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 18799410e..31d883549 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -46,16 +46,16 @@ public class EveryTestPreprocessing { // tag::setup[] private MockMvc mockMvc; - private RestDocumentationResultHandler document; + private RestDocumentationResultHandler documentationHandler; @Before public void setup() { - this.document = document("{method-name}", // <1> + this.documentationHandler = document("{method-name}", // <1> preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.document) // <2> + .alwaysDo(this.documentationHandler) // <2> .build(); } @@ -63,10 +63,11 @@ public void setup() { public void use() throws Exception { // tag::use[] - this.document.snippets( // <1> - links(linkWithRel("self").description("Canonical self link"))); - this.mockMvc.perform(get("/")) // <2> - .andExpect(status().isOk()); + this.mockMvc.perform(get("/")) // <1> + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( // <2> + links(linkWithRel("self").description("Canonical self link")) + )); // end::use[] } diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index e143bcc26..43ef84e79 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -44,16 +44,16 @@ public class EveryTestPreprocessing { // tag::setup[] private RequestSpecification spec; - private RestDocumentationFilter document; + private RestDocumentationFilter documentationFilter; @Before public void setup() { - this.document = document("{method-name}", + this.documentationFilter = document("{method-name}", preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); // <1> this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(this.document)// <2> + .addFilter(this.documentationFilter)// <2> .build(); } @@ -61,9 +61,9 @@ public void setup() { public void use() throws Exception { // tag::use[] - this.document.snippets( // <1> - links(linkWithRel("self").description("Canonical self link"))); - RestAssured.given(this.spec) // <2> + RestAssured.given(this.spec) // <1> + .filter(this.documentationFilter.document( // <2> + links(linkWithRel("self").description("Canonical self link")))) .when().get("/") .then().assertThat().statusCode(is(200)); // end::use[] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index 52f9cb094..8a2b0402c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -41,6 +41,7 @@ * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson + * @since 1.1 */ public final class RestDocumentationGenerator { @@ -205,11 +206,27 @@ public void handle(REQ request, RESP response, Map configuration * called. * * @param snippets the snippets to add + * @deprecated since 1.1 in favor of {@link #withSnippets(Snippet...)} */ + @Deprecated public void addSnippets(Snippet... snippets) { this.additionalSnippets.addAll(Arrays.asList(snippets)); } + /** + * Creates a new {@link RestDocumentationGenerator} with the same configuration as + * this one other than its snippets. The new generator will use the given + * {@code snippets}. + * + * @param snippets the snippets + * @return the new generator + */ + public RestDocumentationGenerator withSnippets(Snippet... snippets) { + return new RestDocumentationGenerator<>(this.identifier, this.requestConverter, + this.responseConverter, this.requestPreprocessor, + this.responsePreprocessor, snippets); + } + @SuppressWarnings("unchecked") private List getSnippets(Map configuration) { List combinedSnippets = new ArrayList<>(this.snippets); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index fd9b032d3..133974a75 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -35,6 +35,8 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,6 +45,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link RestDocumentationGenerator}. @@ -105,6 +108,7 @@ public void defaultSnippetsAreCalled() throws IOException { } @Test + @Deprecated public void additionalSnippetsAreCalled() throws IOException { given(this.requestConverter.convert(this.request)) .willReturn(this.operationRequest); @@ -123,6 +127,33 @@ public void additionalSnippetsAreCalled() throws IOException { verifySnippetInvocation(additionalSnippet2, configuration); } + @Test + public void newGeneratorOnlyCallsItsSnippets() throws IOException { + OperationRequestPreprocessor requestPreprocessor = mock( + OperationRequestPreprocessor.class); + OperationResponsePreprocessor responsePreprocessor = mock( + OperationResponsePreprocessor.class); + given(this.requestConverter.convert(this.request)) + .willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)) + .willReturn(this.operationResponse); + given(requestPreprocessor.preprocess(this.operationRequest)) + .willReturn(this.operationRequest); + given(responsePreprocessor.preprocess(this.operationResponse)) + .willReturn(this.operationResponse); + Snippet additionalSnippet1 = mock(Snippet.class); + Snippet additionalSnippet2 = mock(Snippet.class); + RestDocumentationGenerator generator = new RestDocumentationGenerator<>( + "id", this.requestConverter, this.responseConverter, requestPreprocessor, + responsePreprocessor, this.snippet); + HashMap configuration = new HashMap<>(); + generator.withSnippets(additionalSnippet1, additionalSnippet2) + .handle(this.request, this.response, configuration); + verifyNoMoreInteractions(this.snippet); + verifySnippetInvocation(additionalSnippet1, configuration); + verifySnippetInvocation(additionalSnippet2, configuration); + } + private void verifySnippetInvocation(Snippet snippet, Map attributes) throws IOException { verifySnippetInvocation(snippet, attributes, 1); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index ac43bdd68..5a85fc5c2 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.mockmvc; +import java.util.HashMap; import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; @@ -59,10 +60,44 @@ public void handle(MvcResult result) throws Exception { * * @param snippets the snippets to add * @return this {@code RestDocumentationResultHandler} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} */ + @Deprecated public RestDocumentationResultHandler snippets(Snippet... snippets) { this.delegate.addSnippets(snippets); return this; } + /** + * Creates a new {@link RestDocumentationResultHandler} that will produce + * documentation using the given {@code snippets}. + * + * @param snippets the snippets + * @return the new result handler + */ + public RestDocumentationResultHandler document(Snippet... snippets) { + return new RestDocumentationResultHandler(this.delegate.withSnippets(snippets)) { + + @Override + public void handle(MvcResult result) throws Exception { + @SuppressWarnings("unchecked") + Map configuration = new HashMap<>( + (Map) result.getRequest() + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION)); + configuration.remove( + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + getDelegate().handle(result.getRequest(), result.getResponse(), + configuration); + } + }; + } + + /** + * Returns the {@link RestDocumentationGenerator} that is used as a delegate. + * + * @return the delegate + */ + protected final RestDocumentationGenerator getDelegate() { + return this.delegate; + } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5068d34fc..5d70cfb57 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,6 +56,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -329,6 +331,24 @@ public void multiStep() throws Exception { "http-response.adoc", "curl-request.adoc"); } + @Test + public void alwaysDoWithAdditionalSnippets() throws Exception { + RestDocumentationResultHandler documentation = document("{method-name}-{step}"); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(documentation).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(documentation.document( + responseHeaders(headerWithName("a").description("one")))); + + assertExpectedSnippetFilesExist( + new File( + "build/generated-snippets/always-do-with-additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "response-headers.adoc"); + } + @Test public void preprocessedRequest() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 3ee03d3fb..0d37351e8 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -35,7 +35,7 @@ * * @author Andy Wilkinson */ -public final class RestDocumentationFilter implements Filter { +public class RestDocumentationFilter implements Filter { static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; @@ -48,10 +48,27 @@ public final class RestDocumentationFilter implements Filter { } @Override - public Response filter(FilterableRequestSpecification requestSpec, + public final Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext context) { Response response = context.next(requestSpec, responseSpec); + Map configuration = getConfiguration(requestSpec, context); + + this.delegate.handle(requestSpec, response, configuration); + + return response; + } + + /** + * Returns the configuration that should be used when calling the delgate. The + * configuration is derived from the given {@code requestSpec} and {@code context}. + * + * @param requestSpec the request specification + * @param context the filter context + * @return the configuration + */ + protected Map getConfiguration( + FilterableRequestSpecification requestSpec, FilterContext context) { Map configuration = new HashMap<>( context.>getValue(CONTEXT_KEY_CONFIGURATION)); configuration.put(RestDocumentationContext.class.getName(), @@ -59,10 +76,7 @@ public Response filter(FilterableRequestSpecification requestSpec, RestDocumentationContext.class.getName())); configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); - - this.delegate.handle(requestSpec, response, configuration); - - return response; + return configuration; } /** @@ -71,10 +85,35 @@ public Response filter(FilterableRequestSpecification requestSpec, * * @param snippets the snippets to add * @return this {@code RestDocumentationFilter} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} */ - public RestDocumentationFilter snippets(Snippet... snippets) { + @Deprecated + public final RestDocumentationFilter snippets(Snippet... snippets) { this.delegate.addSnippets(snippets); return this; } + /** + * Creates a new {@link RestDocumentationFilter} that will produce documentation using + * the given {@code snippets}. + * + * @param snippets the snippets + * @return the new result handler + */ + public final RestDocumentationFilter document(Snippet... snippets) { + return new RestDocumentationFilter(this.delegate.withSnippets(snippets)) { + + @Override + protected Map getConfiguration( + FilterableRequestSpecification requestSpec, FilterContext context) { + Map configuration = super.getConfiguration(requestSpec, + context); + configuration.remove( + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + return configuration; + } + + }; + } + } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 658793ee7..27c055ae0 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -52,6 +52,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; @@ -240,6 +242,23 @@ public void multiStep() throws Exception { "http-response.adoc", "curl-request.adoc"); } + @Test + public void additionalSnippets() throws Exception { + RestDocumentationFilter documentation = document("{method-name}-{step}"); + RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(documentation).build(); + given(spec) + .filter(documentation + .document(responseHeaders(headerWithName("a").description("one"), + headerWithName("Foo").description("two")))) + .get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "response-headers.adoc"); + } + @Test public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); From 0446c99a101a423c99003c8fa5a6a551f374865c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 11:26:02 +0100 Subject: [PATCH 0291/1059] Polishing - Correct copyright years - Add `@since` to javadoc of new public classes - Make new classes final where appropriate --- config/eclipse/org.eclipse.jdt.ui.prefs | 2 +- docs/src/test/java/com/example/SnippetReuse.java | 2 +- .../test/java/com/example/mockmvc/MockMvcSnippetReuse.java | 6 +++--- .../com/example/restassured/RestAssuredSnippetReuse.java | 6 +++--- .../springframework/restdocs/ManualRestDocumentation.java | 2 +- .../restdocs/RestDocumentationContextProvider.java | 2 +- .../org/springframework/restdocs/cli/CliDocumentation.java | 3 ++- .../springframework/restdocs/cli/CliOperationRequest.java | 2 +- .../springframework/restdocs/cli/CurlRequestSnippet.java | 1 + .../springframework/restdocs/cli/HttpieRequestSnippet.java | 1 + .../springframework/restdocs/config/AbstractConfigurer.java | 1 + .../restdocs/config/AbstractNestedConfigurer.java | 1 + .../springframework/restdocs/config/NestedConfigurer.java | 1 + .../restdocs/config/RestDocumentationConfigurer.java | 1 + .../restdocs/config/SnippetConfiguration.java | 1 + .../springframework/restdocs/config/SnippetConfigurer.java | 1 + .../generate/RestDocumentationGenerationException.java | 3 ++- .../restdocs/generate/RestDocumentationGenerator.java | 2 +- .../org/springframework/restdocs/generate/package-info.java | 4 ++-- .../restdocs/operation/ConversionException.java | 3 ++- .../restdocs/operation/RequestConverter.java | 3 ++- .../restdocs/operation/ResponseConverter.java | 3 ++- .../operation/preprocess/OperationPreprocessorAdapter.java | 2 +- .../ParametersModifyingOperationPreprocessor.java | 2 +- .../restdocs/request/RequestPartDescriptor.java | 1 + .../restdocs/request/RequestPartsSnippet.java | 1 + .../restdocs/snippet/PlaceholderResolverFactory.java | 2 +- .../RestDocumentationContextPlaceholderResolverFactory.java | 2 +- .../springframework/restdocs/templates/TemplateFormat.java | 1 + .../springframework/restdocs/templates/TemplateFormats.java | 1 + .../mustache/AsciidoctorTableCellContentLambda.java | 1 + .../org/springframework/restdocs/AbstractSnippetTests.java | 2 +- .../restdocs/RestDocumentationGeneratorTests.java | 2 +- .../restdocs/cli/QueryStringParserTests.java | 2 +- .../restdocs/headers/RequestHeadersSnippetFailureTests.java | 2 +- .../headers/ResponseHeadersSnippetFailureTests.java | 2 +- .../restdocs/hypermedia/LinksSnippetFailureTests.java | 2 +- .../ParametersModifyingOperationPreprocessorTests.java | 2 +- .../payload/AsciidoctorRequestFieldsSnippetTests.java | 2 +- .../restdocs/payload/RequestFieldsSnippetFailureTests.java | 2 +- .../restdocs/payload/ResponseFieldsSnippetFailureTests.java | 2 +- .../restdocs/request/PathParametersSnippetFailureTests.java | 2 +- .../request/RequestParametersSnippetFailureTests.java | 2 +- .../restdocs/request/RequestPartsSnippetFailureTests.java | 2 +- .../restdocs/snippet/TemplatedSnippetTests.java | 2 +- .../restdocs/mockmvc/MockMvcRequestConverter.java | 1 - .../mockmvc/MockMvcRestDocumentationConfigurer.java | 5 +++-- .../restdocs/mockmvc/MockMvcSnippetConfigurer.java | 3 ++- .../restdocs/restassured/RestAssuredRestDocumentation.java | 1 + .../restassured/RestAssuredRestDocumentationConfigurer.java | 1 + .../restdocs/restassured/RestAssuredSnippetConfigurer.java | 1 + .../restdocs/restassured/RestDocumentationFilter.java | 1 + .../operation/preprocess/RestAssuredPreprocessors.java | 3 ++- .../preprocess/UriModifyingOperationPreprocessor.java | 3 ++- .../restassured/RestAssuredRequestConverterTests.java | 2 +- .../RestAssuredRestDocumentationConfigurerTests.java | 2 +- .../preprocess/UriModifyingOperationPreprocessorTests.java | 2 +- 57 files changed, 71 insertions(+), 46 deletions(-) diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index 429b30236..d9bb135cd 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -70,7 +70,7 @@ org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true diff --git a/docs/src/test/java/com/example/SnippetReuse.java b/docs/src/test/java/com/example/SnippetReuse.java index 6ab40ddb6..37ecc1acf 100644 --- a/docs/src/test/java/com/example/SnippetReuse.java +++ b/docs/src/test/java/com/example/SnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java index 34dc3ae1b..f76eea97e 100644 --- a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java +++ b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -27,9 +27,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class MockMvcSnippetReuse extends SnippetReuse { - + private MockMvc mockMvc; - + public void documentation() throws Exception { // tag::use[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java index c792a7fbc..770ed4827 100644 --- a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -26,9 +26,9 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RestAssuredSnippetReuse extends SnippetReuse { - + private RequestSpecification spec; - + public void documentation() throws Exception { // tag::use[] RestAssured.given(this.spec) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index ff12d5910..890d9a4cd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java index 5dbe7a78c..e2a94aa59 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java index 9b6b7675f..b22a2b04b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -27,6 +27,7 @@ * @author Andy Wilkinson * @author Paul-Christian Volkmer * @author Raman Gupta + * @since 1.1.0 */ public abstract class CliDocumentation { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 99ce6aeba..97d64c256 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 5112b6dc5..f077c115a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -39,6 +39,7 @@ * @author Paul-Christian Volkmer * @see CliDocumentation#curlRequest() * @see CliDocumentation#curlRequest(Map) + * @since 1.1.0 */ public class CurlRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 0d132e417..981ea5497 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -40,6 +40,7 @@ * @author Andy Wilkinson * @see CliDocumentation#httpieRequest() * @see CliDocumentation#httpieRequest(Map) + * @since 1.1.0 */ public class HttpieRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index 7a8a9e03e..fd46b3768 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -25,6 +25,7 @@ * configuration implementation. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class AbstractConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java index 30e23a321..0af74d652 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -21,6 +21,7 @@ * * @param The type of the configurer's parent * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class AbstractNestedConfigurer extends AbstractConfigurer implements NestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java index 1ba13b80f..8d628b684 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -21,6 +21,7 @@ * * @param The parent's type * @author Andy Wilkinson + * @since 1.1.0 */ interface NestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index c6cb1ffc4..59444e655 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -39,6 +39,7 @@ * @param The concrete type of this configurer, to be returned from methods that * support chaining * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestDocumentationConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java index a1ced2167..caee04ca1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -22,6 +22,7 @@ * An encapsulation of the configuration for documentation snippets. * * @author Andy Wilkinson + * @since 1.1.0 */ class SnippetConfiguration { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index d6475aadc..0da0b48c9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -35,6 +35,7 @@ * @param The type of the configurer's parent * @param The concrete type of the configurer to be returned from chained methods * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java index e1e76d8c2..2db29ea00 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -21,6 +21,7 @@ * generation. * * @author Andy Wilkinson + * @since 1.1.0 */ public class RestDocumentationGenerationException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index 8a2b0402c..c169b9d8a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -41,7 +41,7 @@ * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public final class RestDocumentationGenerator { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java index 073c2dd8f..28a85f0d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -15,6 +15,6 @@ */ /** - * Classes that drive the generation of the documentaiton snippets. + * Classes that drive the generation of the documentation snippets. */ package org.springframework.restdocs.generate; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java index 397f79d9e..8bf4643cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -24,6 +24,7 @@ * @author Andy Wilkinson * @see RequestConverter#convert(Object) * @see ResponseConverter#convert(Object) + * @since 1.1.0 */ public class ConversionException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java index 5c2377d5a..f7561ae58 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,7 @@ * * @param The implementation-specific request type * @author Andy Wilkinson + * @since 1.1.0 */ public interface RequestConverter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java index 48a76be28..d294e68d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,7 @@ * * @param The implementation-specific response type * @author Andy Wilkinson + * @since 1.1.0 */ public interface ResponseConverter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java index 544de0f45..bf2aadfb7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java index 5006c73e3..71759dad9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java index 789649607..7cdb72bfa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -23,6 +23,7 @@ * * @author Andy Wilkinson * @see RequestDocumentation#partWithName + * @since 1.1.0 */ public class RequestPartDescriptor extends IgnorableDescriptor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index c55bca4e2..1bf6cc35b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -42,6 +42,7 @@ * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) + * @since 1.1.0 */ public class RequestPartsSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java index 6aec68d7b..5683346b8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java @@ -23,7 +23,7 @@ * A factory for creating {@link PlaceholderResolver} instances. * * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public interface PlaceholderResolverFactory { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java index 0ddb24562..c364b14d6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java @@ -24,7 +24,7 @@ * {@link RestDocumentationContextPlaceholderResolver} instances. * * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public final class RestDocumentationContextPlaceholderResolverFactory implements PlaceholderResolverFactory { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java index 972103607..fa75b5f5b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java @@ -21,6 +21,7 @@ * as Asciidoctor or Markdown. * * @author Andy Wilkinson + * @since 1.1.0 */ public interface TemplateFormat { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java index 537f01ed5..1b76d0c0d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java @@ -20,6 +20,7 @@ * An enumeration of the built-in formats for which templates are provuded. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class TemplateFormats { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java index 7881c6e08..a218da0e2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java @@ -27,6 +27,7 @@ * formatting. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class AsciidoctorTableCellContentLambda implements Lambda { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 8969e0bcb..0adc3d0b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index 133974a75..f10712153 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java index 6a953e16a..3f30bd89e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index 47b2024fe..33ccabb27 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index cee2ebc22..997a3bc8a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 217b753e5..b8f994703 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java index 4d8035f39..ebcef8ce1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 070389ce2..c11243c5c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 8b2760a9a..eeab98cfa 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 34770b618..98a31d45a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 0abd322bc..99ab6a0ba 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index 44d3165fe..79cea42b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index 93de349a5..b349f0f03 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index 4213da4e5..f64034c67 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index ba96164ec..e933a0e0a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -50,7 +50,6 @@ * {@link MockHttpServletRequest}. * * @author Andy Wilkinson - * */ class MockMvcRequestConverter implements RequestConverter { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index a1a023c46..202d393f9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -33,8 +33,9 @@ * A MockMvc-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @since 1.1.0 */ -public class MockMvcRestDocumentationConfigurer extends +public final class MockMvcRestDocumentationConfigurer extends RestDocumentationConfigurer implements MockMvcConfigurer { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java index a46302c01..a439bddc4 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java @@ -26,8 +26,9 @@ * A configurer that can be used to configure the generated documentation snippets. * * @author Andy Wilkinson + * @since 1.1.0 */ -public class MockMvcSnippetConfigurer extends +public final class MockMvcSnippetConfigurer extends SnippetConfigurer implements MockMvcConfigurer { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 92aa38641..2cb6b4fa6 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -26,6 +26,7 @@ * Static factory methods for documenting RESTful APIs using REST Assured. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestAssuredRestDocumentation { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 8ede6f745..6e64aa6fe 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -33,6 +33,7 @@ * A REST Assured-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class RestAssuredRestDocumentationConfigurer extends RestDocumentationConfigurer diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java index 63ee75da5..3e1f5dee0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -29,6 +29,7 @@ * using REST Assured. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class RestAssuredSnippetConfigurer extends SnippetConfigurer diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 0d37351e8..97387058b 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -34,6 +34,7 @@ * A REST Assured {@link Filter} for documenting RESTful APIs. * * @author Andy Wilkinson + * @since 1.1.0 */ public class RestDocumentationFilter implements Filter { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java index 5e5c1f9f9..b124544c1 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -28,6 +28,7 @@ * {@link OperationResponse response} before it is documented. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestAssuredPreprocessors { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 9063ac948..3c7b109d0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -53,6 +53,7 @@ * * * @author Andy Wilkinson + * @since 1.1.0 */ public final class UriModifyingOperationPreprocessor implements OperationPreprocessor { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 771fb60f5..389a878aa 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 5da692491..d60b85ff3 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 60bda2db8..8ce532886 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. From 251d93ddc8ffd29eb323dbf8bc0120787f9c7876 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 11:34:57 +0100 Subject: [PATCH 0292/1059] Correct ordering of Javadoc tags --- .../org/springframework/restdocs/cli/CurlRequestSnippet.java | 2 +- .../org/springframework/restdocs/cli/HttpieRequestSnippet.java | 2 +- .../springframework/restdocs/operation/ConversionException.java | 2 +- .../springframework/restdocs/request/RequestPartDescriptor.java | 2 +- .../springframework/restdocs/request/RequestPartsSnippet.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index f077c115a..73eeb72a3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -37,9 +37,9 @@ * * @author Andy Wilkinson * @author Paul-Christian Volkmer + * @since 1.1.0 * @see CliDocumentation#curlRequest() * @see CliDocumentation#curlRequest(Map) - * @since 1.1.0 */ public class CurlRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 981ea5497..57673b9c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -38,9 +38,9 @@ * * @author Raman Gupta * @author Andy Wilkinson + * @since 1.1.0 * @see CliDocumentation#httpieRequest() * @see CliDocumentation#httpieRequest(Map) - * @since 1.1.0 */ public class HttpieRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java index 8bf4643cf..96b6c5e59 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -22,9 +22,9 @@ * during conversion. * * @author Andy Wilkinson + * @since 1.1.0 * @see RequestConverter#convert(Object) * @see ResponseConverter#convert(Object) - * @since 1.1.0 */ public class ConversionException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java index 7cdb72bfa..5ca4e68bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -22,8 +22,8 @@ * A descriptor of a request part. * * @author Andy Wilkinson - * @see RequestDocumentation#partWithName * @since 1.1.0 + * @see RequestDocumentation#partWithName */ public class RequestPartDescriptor extends IgnorableDescriptor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index 1bf6cc35b..7dc97e5a5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -38,11 +38,11 @@ * A {@link Snippet} that documents the request parts supported by a RESTful resource. * * @author Andy Wilkinson + * @since 1.1.0 * @see RequestDocumentation#requestParts(RequestPartDescriptor...) * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) - * @since 1.1.0 */ public class RequestPartsSnippet extends TemplatedSnippet { From 26ff2ec8a51421e15acf032bd7f19968ca1697a7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 12:02:53 +0100 Subject: [PATCH 0293/1059] Update HATEOAS sample to use new API for generating extra snippets See gh-249 --- .../com/example/notes/ApiDocumentation.java | 300 +++++++++--------- 1 file changed, 149 insertions(+), 151 deletions(-) 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 90abbdc62..b3cbb58f5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -34,7 +34,6 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -73,7 +72,7 @@ public class ApiDocumentation { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - private RestDocumentationResultHandler document; + private RestDocumentationResultHandler documentationHandler; @Autowired private NoteRepository noteRepository; @@ -91,77 +90,73 @@ public class ApiDocumentation { @Before public void setUp() { - this.document = document("{method-name}", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint())); + this.documentationHandler = document("{method-name}", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.document) - .build(); + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(this.documentationHandler) + .build(); } @Test public void headersExample() throws Exception { - this.document.snippets(responseHeaders( - headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); - - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()); + this.mockMvc + .perform(get("/")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( + responseHeaders( + headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`")))); } @Test public void errorExample() throws Exception { - this.document.snippets(responseFields( - 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"))); - this.mockMvc .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist")) - .andDo(print()).andExpect(status().isBadRequest()) + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, "The tag 'http://localhost:8080/tags/123' does not exist")) + .andExpect(status().isBadRequest()) .andExpect(jsonPath("error", is("Bad Request"))) .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))); + .andExpect(jsonPath("path", is(notNullValue()))) + .andDo(this.documentationHandler.document( + responseFields( + 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.document.snippets( + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")), + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>")), responseFields( - fieldWithPath("_links").description("<> to other resources"))); - - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()); + fieldWithPath("_links").description("<> to other resources")))); } @Test public void notesListExample() throws Exception { this.noteRepository.deleteAll(); - createNote("REST maturity model", - "http://martinfowler.com/articles/richardsonMaturityModel.html"); - createNote("Hypertext Application Language (HAL)", - "http://stateless.co/hal_specification.html"); + createNote("REST maturity model", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - this.document.snippets( + this.mockMvc + .perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.notes").description("An array of <>"))); - - this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()); + fieldWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -170,11 +165,11 @@ public void notesCreateExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map note = new HashMap(); note.put("title", "REST maturity model"); @@ -183,16 +178,17 @@ public void notesCreateExample() throws Exception { ConstrainedFields fields = new ConstrainedFields(NoteInput.class); - this.document.snippets( + this.mockMvc + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect( + status().isCreated()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("title").description("The title of the note"), - fields.withPath("body").description("The body of the note"), - fields.withPath("tags").description("An array of tag resource URIs"))); - - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()); + fields.withPath("title").description("The title of the note"), + fields.withPath("body").description("The body of the note"), + fields.withPath("tags").description("An array of tag resource URIs")))); } @Test @@ -201,11 +197,11 @@ public void noteGetExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map note = new HashMap(); note.put("title", "REST maturity model"); @@ -213,27 +209,27 @@ public void noteGetExample() throws Exception { note.put("tags", Arrays.asList(tagLocation)); String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); - this.document.snippets( - links( - linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources"))); - - this.mockMvc.perform(get(noteLocation)) + this.mockMvc + .perform(get(noteLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("title", is(note.get("title")))) .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) + .andDo(this.documentationHandler.document( + links( + linkWithRel("self").description("This <>"), + linkWithRel("note-tags").description("This note's <>")), + responseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("<> to other resources")))); } @@ -246,12 +242,12 @@ public void tagsListExample() throws Exception { createTag("Hypermedia"); createTag("HTTP"); - this.document.snippets( + this.mockMvc + .perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.tags").description("An array of <>"))); - - this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()); + fieldWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -261,14 +257,14 @@ public void tagsCreateExample() throws Exception { ConstrainedFields fields = new ConstrainedFields(TagInput.class); - this.document.snippets( + this.mockMvc + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("name").description("The name of the tag"))); - - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()); + fields.withPath("name").description("The name of the tag")))); } @Test @@ -278,50 +274,52 @@ public void noteUpdateExample() throws Exception { note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); + + this.mockMvc + .perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); Map tag = new HashMap(); tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map noteUpdate = new HashMap(); noteUpdate.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class); - - this.document.snippets( + + this.mockMvc + .perform(patch(noteLocation) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("title") - .description("The title of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("body") - .description("The body of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("tags") - .description("An array of tag resource URIs"))); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()); + fields.withPath("title") + .description("The title of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("body") + .description("The body of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("tags") + .description("An array of tag resource URIs")))); } @Test @@ -330,23 +328,23 @@ public void tagGetExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.document.snippets( - links( - linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources"))); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); - this.mockMvc.perform(get(tagLocation)) + this.mockMvc + .perform(get(tagLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))); + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(this.documentationHandler.document( + links( + linkWithRel("self").description("This <>"), + linkWithRel("tagged-notes").description("The <> that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -355,25 +353,25 @@ public void tagUpdateExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map tagUpdate = new HashMap(); tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); - this.document.snippets( + this.mockMvc + .perform(patch(tagLocation) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("name").description("The name of the tag"))); - - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()); + fields.withPath("name").description("The name of the tag")))); } private void createNote(String title, String body) { From a9305cba8764f5e61a26c64988cb4bab90610671 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 25 May 2016 12:26:06 +0000 Subject: [PATCH 0294/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a651766f6..cecc0ca16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.2.BUILD-SNAPSHOT +version=1.0.3.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From c1d245f7d438e557217b7633a31e839a3ff4e632 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 14:18:55 +0100 Subject: [PATCH 0295/1059] Add link to restdocs-wiremock to the README Closes gh-244 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 18fd83473..50f3ebe86 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ There are several that you can contribute to Spring REST Docs: - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag. - Chat with fellow users [on Gitter][16]. +## Third-party extensions + +| Name | Description | +| ---- | ----------- | +| [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | + ## Licence Spring REST Docs is open source software released under the [Apache 2.0 license][14]. @@ -52,4 +58,6 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [13]: CONTRIBUTING.md [14]: http://www.apache.org/licenses/LICENSE-2.0.html [15]: http://stackoverflow.com/tags/spring-restdocs -[16]: https://gitter.im/spring-projects/spring-restdocs \ No newline at end of file +[16]: https://gitter.im/spring-projects/spring-restdocs +[17]: https://github.com/ePages-de/restdocs-wiremock +[18]: http://wiremock.org/ \ No newline at end of file From 64803b5fae42bfb63ed19c7b4467600e66d534c2 Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 30 May 2016 12:22:35 -0500 Subject: [PATCH 0296/1059] Add Grails sample app Closes gh-250 --- docs/src/docs/asciidoc/getting-started.adoc | 4 + samples/rest-notes-grails/.gitignore | 17 ++ samples/rest-notes-grails/LICENSE | 201 ++++++++++++++++++ samples/rest-notes-grails/README.md | 2 + samples/rest-notes-grails/build.gradle | 79 +++++++ samples/rest-notes-grails/gradle.properties | 2 + .../rest-notes-grails/gradle/restdocs.gradle | 27 +++ .../grails-app/conf/application.yml | 98 +++++++++ .../grails-app/conf/logback.groovy | 23 ++ .../grails-app/conf/spring/resources.groovy | 3 + .../grails-app/controllers/UrlMappings.groovy | 8 + .../com/example/IndexController.groovy | 31 +++ .../InternalServerErrorController.groovy | 11 + .../com/example/NotFoundController.groovy | 11 + .../grails-app/domain/com/example/Note.groovy | 16 ++ .../grails-app/domain/com/example/Tag.groovy | 15 ++ .../grails-app/i18n/messages.properties | 56 +++++ .../grails-app/init/BootStrap.groovy | 14 ++ .../init/com/example/Application.groovy | 10 + samples/rest-notes-grails/src/docs/index.adoc | 148 +++++++++++++ .../com/example/ApiDocumentationSpec.groovy | 146 +++++++++++++ 21 files changed, 922 insertions(+) create mode 100644 samples/rest-notes-grails/.gitignore create mode 100644 samples/rest-notes-grails/LICENSE create mode 100644 samples/rest-notes-grails/README.md create mode 100644 samples/rest-notes-grails/build.gradle create mode 100644 samples/rest-notes-grails/gradle.properties create mode 100644 samples/rest-notes-grails/gradle/restdocs.gradle create mode 100644 samples/rest-notes-grails/grails-app/conf/application.yml create mode 100644 samples/rest-notes-grails/grails-app/conf/logback.groovy create mode 100644 samples/rest-notes-grails/grails-app/conf/spring/resources.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy create mode 100644 samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy create mode 100644 samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy create mode 100644 samples/rest-notes-grails/grails-app/i18n/messages.properties create mode 100644 samples/rest-notes-grails/grails-app/init/BootStrap.groovy create mode 100644 samples/rest-notes-grails/grails-app/init/com/example/Application.groovy create mode 100644 samples/rest-notes-grails/src/docs/index.adoc create mode 100644 samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 094c64c29..2a1d161cb 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -18,6 +18,10 @@ If you want to jump straight in, a number of sample applications are available: | Gradle | Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. +| {samples}/rest-notes-grails[Grails] +| Gradle +| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and https://github.com/spockframework/spock[Spock] + | {samples}/rest-notes-slate[Slate] | Gradle | Demonstrates the use of Spring REST Docs with Markdown and diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore new file mode 100644 index 000000000..a521ca89f --- /dev/null +++ b/samples/rest-notes-grails/.gitignore @@ -0,0 +1,17 @@ +Thumbs.db +.DS_Store +.gradle +build/ +classes/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +gradlew* +gradle/wrapper + + +src/docs/generated-snippets diff --git a/samples/rest-notes-grails/LICENSE b/samples/rest-notes-grails/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/samples/rest-notes-grails/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md new file mode 100644 index 000000000..06c410f79 --- /dev/null +++ b/samples/rest-notes-grails/README.md @@ -0,0 +1,2 @@ +# grails-spring-restdocs-example +Example of adding spring rest docs to sample grails project diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle new file mode 100644 index 000000000..cc8b269ba --- /dev/null +++ b/samples/rest-notes-grails/build.gradle @@ -0,0 +1,79 @@ +buildscript { + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + maven { url 'https://repo.spring.io/libs-snapshot' } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails.plugins:hibernate:4.3.10.5" + classpath 'org.ajoberstar:gradle-git:1.1.0' + } +} + +plugins { + id "io.spring.dependency-management" version "0.5.4.RELEASE" + id 'org.asciidoctor.convert' version '1.5.3' +} + +version "0.1" +group "com.example" + +apply plugin: "spring-boot" +apply plugin: "war" +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: "org.grails.grails-web" + +ext { + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion +} + +repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } +} + +dependencyManagement { + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false +} + +dependencies { + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-plugin-url-mappings" + compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-interceptors" + compile "org.grails:grails-plugin-services" + compile "org.grails:grails-plugin-datasource" + compile "org.grails:grails-plugin-databinding" + compile "org.grails:grails-plugin-async" + compile "org.grails:grails-web-boot" + compile "org.grails:grails-logging" + + compile "org.grails.plugins:hibernate" + compile "org.grails.plugins:cache" + compile "org.hibernate:hibernate-ehcache" + + runtime "com.h2database:h2" + + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + + console "org.grails:grails-console" +} + +task wrapper(type: Wrapper) { + gradleVersion = gradleWrapperVersion +} + +apply from: 'gradle/restdocs.gradle' diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties new file mode 100644 index 000000000..1b1c50f8e --- /dev/null +++ b/samples/rest-notes-grails/gradle.properties @@ -0,0 +1,2 @@ +grailsVersion=3.0.15 +gradleWrapperVersion=2.3 diff --git a/samples/rest-notes-grails/gradle/restdocs.gradle b/samples/rest-notes-grails/gradle/restdocs.gradle new file mode 100644 index 000000000..9b655d2ec --- /dev/null +++ b/samples/rest-notes-grails/gradle/restdocs.gradle @@ -0,0 +1,27 @@ +dependencies { + testCompile 'org.springframework.restdocs:spring-restdocs-restassured:1.1.0.M1' +} + +ext { + snippetsDir = file('src/docs/generated-snippets') +} + +task cleanTempDirs(type: Delete) { + delete fileTree(dir: 'src/docs/generated-snippets') +} + +test { + dependsOn cleanTempDirs + outputs.dir snippetsDir +} + +asciidoctor { + mustRunAfter test + mustRunAfter integrationTest + inputs.dir snippetsDir + sourceDir = file('src/docs') + separateOutputDirs = false + attributes 'snippets': snippetsDir +} + +build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml new file mode 100644 index 000000000..6d7d00e8a --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -0,0 +1,98 @@ +--- +grails: + profile: web-api + codegen: + defaultPackage: com.example +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + hibernate: + cache: + queries: false + +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: update + url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + properties: + jmxEnabled: true + initialSize: 5 + maxActive: 50 + minIdle: 5 + maxIdle: 25 + maxWait: 10000 + maxAge: 600000 + timeBetweenEvictionRunsMillis: 5000 + minEvictableIdleTimeMillis: 60000 + validationQuery: SELECT 1 + validationQueryTimeout: 3 + validationInterval: 15000 + testOnBorrow: true + testWhileIdle: true + testOnReturn: false + jdbcInterceptors: ConnectionState + defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy new file mode 100644 index 000000000..2f7c41c1a --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/logback.groovy @@ -0,0 +1,23 @@ +import grails.util.BuildSettings +import grails.util.Environment + +// See http://logback.qos.ch/manual/groovy.html for details on configuration +appender('STDOUT', ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } +} + +root(ERROR, ['STDOUT']) + +def targetDir = BuildSettings.TARGET_DIR +if (Environment.isDevelopmentMode() && targetDir) { + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) +} diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy new file mode 100644 index 000000000..fa950068b --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy @@ -0,0 +1,3 @@ +// Place your Spring DSL code here +beans = { +} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy new file mode 100644 index 000000000..98be2a3d3 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy @@ -0,0 +1,8 @@ +class UrlMappings { + + static mappings = { + "/"(controller: 'index') + "500"(controller: 'InternalServerError') + "404"(controller: 'NotFound') + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy new file mode 100644 index 000000000..9ed3bc206 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy @@ -0,0 +1,31 @@ +package com.example + +import grails.core.GrailsApplication +import grails.util.Environment + +class IndexController { + + GrailsApplication grailsApplication + + def index() { + render(contentType: 'application/json') { + message = "Welcome to Grails!" + environment = Environment.current.name + appversion = grailsApplication.metadata['info.app.version'] + grailsversion = grailsApplication.metadata['info.app.grailsVersion'] + appprofile = grailsApplication.config.grails?.profile + groovyversion = GroovySystem.getVersion() + jvmversion = System.getProperty('java.version') + controllers = array { + for (c in grailsApplication.controllerClasses) { + controller([name: c.fullName]) + } + } + plugins = array { + for (p in grailsApplication.mainContext.pluginManager.allPlugins) { + plugin([name: p.fullName]) + } + } + } + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy new file mode 100644 index 000000000..d5c169010 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy @@ -0,0 +1,11 @@ +package com.example + +class InternalServerErrorController { + + def index() { + render(contentType: 'application/json') { + error = 500 + message = "Internal server error" + } + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy new file mode 100644 index 000000000..8527921de --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy @@ -0,0 +1,11 @@ +package com.example + +class NotFoundController { + + def index() { + render(contentType: 'application/json') { + error = 404 + message = "Not Found" + } + } +} diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy new file mode 100644 index 000000000..d13fbff54 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy @@ -0,0 +1,16 @@ +package com.example + +import grails.rest.Resource + +@Resource(uri='/notes', formats = ['json', 'xml']) +class Note { + Long id + String title + String body + + static hasMany = [tags: Tag] + static mapping = { + tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] + } +} + diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy new file mode 100644 index 000000000..f9b33d116 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy @@ -0,0 +1,15 @@ +package com.example + +import grails.rest.Resource + +@Resource(uri='/tags', formats = ['json', 'xml']) +class Tag { + Long id + String name + + static hasMany = [notes: Note] + static belongsTo = Note + static mapping = { + notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] + } +} diff --git a/samples/rest-notes-grails/grails-app/i18n/messages.properties b/samples/rest-notes-grails/grails-app/i18n/messages.properties new file mode 100644 index 000000000..b04513621 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/i18n/messages.properties @@ -0,0 +1,56 @@ +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL +default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number +default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address +default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] +default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] +default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] +default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] +default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] +default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] +default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation +default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] +default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] +default.null.message=Property [{0}] of class [{1}] cannot be null +default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique + +default.paginate.prev=Previous +default.paginate.next=Next +default.boolean.true=True +default.boolean.false=False +default.date.format=yyyy-MM-dd HH:mm:ss z +default.number.format=0 + +default.created.message={0} {1} created +default.updated.message={0} {1} updated +default.deleted.message={0} {1} deleted +default.not.deleted.message={0} {1} could not be deleted +default.not.found.message={0} not found with id {1} +default.optimistic.locking.failure=Another user has updated this {0} while you were editing + +default.home.label=Home +default.list.label={0} List +default.add.label=Add {0} +default.new.label=New {0} +default.create.label=Create {0} +default.show.label=Show {0} +default.edit.label=Edit {0} + +default.button.create.label=Create +default.button.edit.label=Edit +default.button.update.label=Update +default.button.delete.label=Delete +default.button.delete.confirm.message=Are you sure? + +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Property {0} must be a valid URL +typeMismatch.java.net.URI=Property {0} must be a valid URI +typeMismatch.java.util.Date=Property {0} must be a valid Date +typeMismatch.java.lang.Double=Property {0} must be a valid number +typeMismatch.java.lang.Integer=Property {0} must be a valid number +typeMismatch.java.lang.Long=Property {0} must be a valid number +typeMismatch.java.lang.Short=Property {0} must be a valid number +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number +typeMismatch.java.math.BigInteger=Property {0} must be a valid number +typeMismatch=Property {0} is type-mismatched diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy new file mode 100644 index 000000000..1fba80026 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy @@ -0,0 +1,14 @@ +import com.example.Note + +class BootStrap { + + def init = { servletContext -> + environments { + test { + new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() + } + } + } + def destroy = { + } +} diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy new file mode 100644 index 000000000..58ad55977 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy @@ -0,0 +1,10 @@ +package com.example + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration + +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/index.adoc new file mode 100644 index 000000000..231af2ecf --- /dev/null +++ b/samples/rest-notes-grails/src/docs/index.adoc @@ -0,0 +1,148 @@ += Grails RESTful Notes API Guide +Andy Wilkinson; Jenn Strater +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +[[overview]] += Overview + +[[overview-http-verbs]] +== HTTP verbs + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP verbs. + +|=== +| Verb | Usage + +| `GET` +| Used to retrieve a resource + +| `POST` +| Used to create a new resource + +| `PATCH` +| Used to update an existing resource, including partial updates + +| `DELETE` +| Used to delete an existing resource +|=== + +[[overview-http-status-codes]] +== HTTP status codes + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP status codes. + +|=== +| Status code | Usage + +| `200 OK` +| The request completed successfully + +| `201 Created` +| A new resource has been created successfully. The resource's URI is available from the response's +`Location` header + +| `204 No Content` +| An update to an existing resource has been applied successfully + +| `400 Bad Request` +| The request was malformed. The response body will include an error providing further information + +| `404 Not Found` +| The requested resource did not exist +|=== + +[[resources]] += Resources + + +[[resources-index]] +== Index + +The index provides the entry point into the service. + + + +[[resources-index-access]] +=== Accessing the index + +A `GET` request is used to access the index + +==== Example request + +include::{snippets}/index-example/curl-request.adoc[] + +==== Response structure + +include::{snippets}/index-example/response-fields.adoc[] + +==== Example response + +include::{snippets}/index-example/http-response.adoc[] + + +[[resources-notes]] +== Notes + +The Notes resources is used to create and list notes + + + +[[resources-notes-list]] +=== Listing notes + +A `GET` request will list all of the service's notes. + +==== Response structure + +include::{snippets}/notes-list-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/notes-list-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-list-example/http-response.adoc[] + + + +[[resources-notes-create]] +=== Creating a note + +A `POST` request is used to create a note + +==== Request structure + +include::{snippets}/notes-create-example/request-fields.adoc[] + +==== Example request + +include::{snippets}/notes-create-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-create-example/http-response.adoc[] + +[[resources-note-retrieve]] +=== Retrieve a note + +A `GET` request will retrieve the details of a note + +==== Response structure + +include::{snippets}/note-get-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/note-get-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/note-get-example/http-response.adoc[] diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy new file mode 100644 index 000000000..949f8a568 --- /dev/null +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -0,0 +1,146 @@ +package com.example + +import org.springframework.restdocs.payload.JsonFieldType + +import static com.jayway.restassured.RestAssured.given +import static org.hamcrest.CoreMatchers.is +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration + +import com.jayway.restassured.builder.RequestSpecBuilder +import com.jayway.restassured.specification.RequestSpecification +import grails.test.mixin.integration.Integration +import grails.transaction.Rollback +import org.junit.Rule +import org.springframework.restdocs.JUnitRestDocumentation +import org.springframework.http.MediaType +import spock.lang.Specification + +@Integration +@Rollback +class ApiDocumentationSpec extends Specification { + @Rule + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + + protected RequestSpecification documentationSpec + + void setup() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)) + .build() + } + + void 'test and document get request for /index'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('index-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('message').description('Welcome to Grails!'), + fieldWithPath('environment').description("The running environment"), + fieldWithPath('appversion').description('version of the app that is running'), + fieldWithPath('grailsversion').description('the version of grails used in this project'), + fieldWithPath('appprofile').description('the profile of grails used in this project'), + fieldWithPath('groovyversion').description('the version of groovy used in this project'), + fieldWithPath('jvmversion').description('the version of the jvm used in this project'), + fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + ))) + .when() + .port(8080) + .get('/') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document notes list request'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-list-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('[].class').description('the class of the resource'), + fieldWithPath('[].id').description('the id of the note'), + fieldWithPath('[].title').description('the title of the note'), + fieldWithPath('[].body').description('the body of the note'), + fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document create new note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .contentType(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-create-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + ), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') + .when() + .port(8080) + .post('/notes') + .then() + .assertThat() + .statusCode(is(201)) + } + + void 'test and document getting specific note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('note-get-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes/1') + .then() + .assertThat() + .statusCode(is(200)) + } +} From 34ec9d523d69a80d7d51d02b25b17a7c9495bcd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 09:52:26 +0100 Subject: [PATCH 0297/1059] Polish "Add Grails sample app" (1338df9) The sole structural change is to rearrange the build slightly so that, like the other samples, the version of the Grails sample's REST Docs dependency is automatically updated. See gh-250 --- build.gradle | 5 + .../build/SampleBuildConfigurer.groovy | 3 + samples/rest-notes-grails/LICENSE | 201 --------------- samples/rest-notes-grails/README.md | 2 +- samples/rest-notes-grails/build.gradle | 126 +++++---- .../rest-notes-grails/gradle/restdocs.gradle | 27 -- .../grails-app/conf/logback.groovy | 38 ++- .../grails-app/conf/spring/resources.groovy | 3 +- .../grails-app/controllers/UrlMappings.groovy | 27 +- .../com/example/IndexController.groovy | 61 +++-- .../InternalServerErrorController.groovy | 29 ++- .../com/example/NotFoundController.groovy | 29 ++- .../grails-app/domain/com/example/Note.groovy | 37 ++- .../grails-app/domain/com/example/Tag.groovy | 37 ++- .../grails-app/init/BootStrap.groovy | 35 ++- .../init/com/example/Application.groovy | 24 +- .../com/example/ApiDocumentationSpec.groovy | 240 ++++++++++-------- 17 files changed, 455 insertions(+), 469 deletions(-) delete mode 100644 samples/rest-notes-grails/LICENSE delete mode 100644 samples/rest-notes-grails/gradle/restdocs.gradle diff --git a/build.gradle b/build.gradle index 38edf1e73..af4d5bdd4 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,10 @@ samples { dependOn 'spring-restdocs-mockmvc:install' dependOn 'spring-restdocs-restassured:install' + restNotesGrails { + workingDir "$projectDir/samples/rest-notes-grails" + } + restNotesSpringHateoas { workingDir "$projectDir/samples/rest-notes-spring-hateoas" } @@ -183,6 +187,7 @@ samples { workingDir "$projectDir/samples/rest-notes-slate" build false } + } task api (type: Javadoc) { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 1be930cf8..69b6f1fd5 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,6 +61,9 @@ public class SampleBuildConfigurer { replaceVersion(new File(this.workingDir, 'build.gradle'), "ext\\['spring-restdocs.version'\\] = '.*'", "ext['spring-restdocs.version'] = '${project.version}'") + replaceVersion(new File(this.workingDir, 'build.gradle'), + "restDocsVersion = \".*\"", + "restDocsVersion = \"${project.version}\"") } } else if (new File(sampleDir, 'pom.xml').isFile()) { diff --git a/samples/rest-notes-grails/LICENSE b/samples/rest-notes-grails/LICENSE deleted file mode 100644 index 8dada3eda..000000000 --- a/samples/rest-notes-grails/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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. diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md index 06c410f79..386292af4 100644 --- a/samples/rest-notes-grails/README.md +++ b/samples/rest-notes-grails/README.md @@ -1,2 +1,2 @@ # grails-spring-restdocs-example -Example of adding spring rest docs to sample grails project +Example of using Spring REST Docs with Grails diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index cc8b269ba..85ac7c4e6 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -1,22 +1,22 @@ buildscript { - ext { - grailsVersion = project.grailsVersion - } - repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } - maven { url 'https://repo.spring.io/libs-snapshot' } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate:4.3.10.5" - classpath 'org.ajoberstar:gradle-git:1.1.0' - } + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + maven { url 'https://repo.spring.io/libs-snapshot' } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails.plugins:hibernate:4.3.10.5" + classpath 'org.ajoberstar:gradle-git:1.1.0' + } } plugins { - id "io.spring.dependency-management" version "0.5.4.RELEASE" - id 'org.asciidoctor.convert' version '1.5.3' + id "io.spring.dependency-management" version "0.5.4.RELEASE" + id 'org.asciidoctor.convert' version '1.5.3' } version "0.1" @@ -29,51 +29,79 @@ apply plugin: 'idea' apply plugin: "org.grails.grails-web" ext { - grailsVersion = project.grailsVersion - gradleWrapperVersion = project.gradleWrapperVersion + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion + restDocsVersion = "1.1.0.BUILD-SNAPSHOT" + snippetsDir = file('src/docs/generated-snippets') } repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + maven { url "https://repo.grails.org/grails/core" } } dependencyManagement { - imports { - mavenBom "org.grails:grails-bom:$grailsVersion" - } - applyMavenExclusions false + dependencies { + dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" + } + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false } dependencies { - compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-starter-actuator" - compile "org.springframework.boot:spring-boot-autoconfigure" - compile "org.springframework.boot:spring-boot-starter-tomcat" - compile "org.grails:grails-plugin-url-mappings" - compile "org.grails:grails-plugin-rest" - compile "org.grails:grails-plugin-interceptors" - compile "org.grails:grails-plugin-services" - compile "org.grails:grails-plugin-datasource" - compile "org.grails:grails-plugin-databinding" - compile "org.grails:grails-plugin-async" - compile "org.grails:grails-web-boot" - compile "org.grails:grails-logging" - - compile "org.grails.plugins:hibernate" - compile "org.grails.plugins:cache" - compile "org.hibernate:hibernate-ehcache" - - runtime "com.h2database:h2" - - testCompile "org.grails:grails-plugin-testing" - testCompile "org.grails.plugins:geb" - - console "org.grails:grails-console" + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-plugin-url-mappings" + compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-interceptors" + compile "org.grails:grails-plugin-services" + compile "org.grails:grails-plugin-datasource" + compile "org.grails:grails-plugin-databinding" + compile "org.grails:grails-plugin-async" + compile "org.grails:grails-web-boot" + compile "org.grails:grails-logging" + + compile "org.grails.plugins:hibernate" + compile "org.grails.plugins:cache" + compile "org.hibernate:hibernate-ehcache" + + runtime "com.h2database:h2" + + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + testCompile 'org.springframework.restdocs:spring-restdocs-restassured' + + console "org.grails:grails-console" } task wrapper(type: Wrapper) { - gradleVersion = gradleWrapperVersion + gradleVersion = gradleWrapperVersion +} + +ext { + snippetsDir = file('src/docs/generated-snippets') +} + +task cleanTempDirs(type: Delete) { + delete fileTree(dir: 'src/docs/generated-snippets') +} + +test { + dependsOn cleanTempDirs + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn integrationTest + inputs.dir snippetsDir + sourceDir = file('src/docs') + separateOutputDirs = false + attributes 'snippets': snippetsDir } -apply from: 'gradle/restdocs.gradle' +build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/gradle/restdocs.gradle b/samples/rest-notes-grails/gradle/restdocs.gradle deleted file mode 100644 index 9b655d2ec..000000000 --- a/samples/rest-notes-grails/gradle/restdocs.gradle +++ /dev/null @@ -1,27 +0,0 @@ -dependencies { - testCompile 'org.springframework.restdocs:spring-restdocs-restassured:1.1.0.M1' -} - -ext { - snippetsDir = file('src/docs/generated-snippets') -} - -task cleanTempDirs(type: Delete) { - delete fileTree(dir: 'src/docs/generated-snippets') -} - -test { - dependsOn cleanTempDirs - outputs.dir snippetsDir -} - -asciidoctor { - mustRunAfter test - mustRunAfter integrationTest - inputs.dir snippetsDir - sourceDir = file('src/docs') - separateOutputDirs = false - attributes 'snippets': snippetsDir -} - -build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy index 2f7c41c1a..c8bc3a402 100644 --- a/samples/rest-notes-grails/grails-app/conf/logback.groovy +++ b/samples/rest-notes-grails/grails-app/conf/logback.groovy @@ -1,23 +1,39 @@ +/* + * Copyright 2014-2016 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. + */ + import grails.util.BuildSettings import grails.util.Environment // See http://logback.qos.ch/manual/groovy.html for details on configuration appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } } root(ERROR, ['STDOUT']) def targetDir = BuildSettings.TARGET_DIR if (Environment.isDevelopmentMode() && targetDir) { - appender("FULL_STACKTRACE", FileAppender) { - file = "${targetDir}/stacktrace.log" - append = true - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } - } - logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) } diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy index fa950068b..4907ee437 100644 --- a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy +++ b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy @@ -1,3 +1,2 @@ // Place your Spring DSL code here -beans = { -} +beans = {} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy index 98be2a3d3..7d8cffe56 100644 --- a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy @@ -1,8 +1,25 @@ +/* + * Copyright 2014-2016 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. + */ + class UrlMappings { - static mappings = { - "/"(controller: 'index') - "500"(controller: 'InternalServerError') - "404"(controller: 'NotFound') - } + static mappings = { + "/"(controller: 'index') + "500"(controller: 'InternalServerError') + "404"(controller: 'NotFound') + } + } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy index 9ed3bc206..01f585182 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2014-2016 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 com.example import grails.core.GrailsApplication @@ -5,27 +21,28 @@ import grails.util.Environment class IndexController { - GrailsApplication grailsApplication + GrailsApplication grailsApplication + + def index() { + render(contentType: 'application/json') { + message = "Welcome to Grails!" + environment = Environment.current.name + appversion = grailsApplication.metadata['info.app.version'] + grailsversion = grailsApplication.metadata['info.app.grailsVersion'] + appprofile = grailsApplication.config.grails?.profile + groovyversion = GroovySystem.getVersion() + jvmversion = System.getProperty('java.version') + controllers = array { + for (c in grailsApplication.controllerClasses) { + controller([name: c.fullName]) + } + } + plugins = array { + for (p in grailsApplication.mainContext.pluginManager.allPlugins) { + plugin([name: p.fullName]) + } + } + } + } - def index() { - render(contentType: 'application/json') { - message = "Welcome to Grails!" - environment = Environment.current.name - appversion = grailsApplication.metadata['info.app.version'] - grailsversion = grailsApplication.metadata['info.app.grailsVersion'] - appprofile = grailsApplication.config.grails?.profile - groovyversion = GroovySystem.getVersion() - jvmversion = System.getProperty('java.version') - controllers = array { - for (c in grailsApplication.controllerClasses) { - controller([name: c.fullName]) - } - } - plugins = array { - for (p in grailsApplication.mainContext.pluginManager.allPlugins) { - plugin([name: p.fullName]) - } - } - } - } } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy index d5c169010..c0e80312c 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy @@ -1,11 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example class InternalServerErrorController { - def index() { - render(contentType: 'application/json') { - error = 500 - message = "Internal server error" - } - } + def index() { + render(contentType: 'application/json') { + error = 500 + message = "Internal server error" + } + } + } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy index 8527921de..039968a93 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy @@ -1,11 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example class NotFoundController { - def index() { - render(contentType: 'application/json') { - error = 404 - message = "Not Found" - } - } + def index() { + render(contentType: 'application/json') { + error = 404 + message = "Not Found" + } + } + } diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy index d13fbff54..140b839cd 100644 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy @@ -1,16 +1,37 @@ +/* + * Copyright 2014-2016 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 com.example import grails.rest.Resource @Resource(uri='/notes', formats = ['json', 'xml']) class Note { - Long id - String title - String body - - static hasMany = [tags: Tag] - static mapping = { - tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] - } + + Long id + + String title + + String body + + static hasMany = [tags: Tag] + + static mapping = { + tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] + } + } diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy index f9b33d116..b0c712c63 100644 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy @@ -1,15 +1,36 @@ +/* + * Copyright 2014-2016 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 com.example import grails.rest.Resource @Resource(uri='/tags', formats = ['json', 'xml']) class Tag { - Long id - String name - - static hasMany = [notes: Note] - static belongsTo = Note - static mapping = { - notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] - } + + Long id + + String name + + static hasMany = [notes: Note] + + static belongsTo = Note + + static mapping = { + notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] + } + } diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy index 1fba80026..0efbc954f 100644 --- a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy +++ b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy @@ -1,14 +1,31 @@ +/* + * Copyright 2014-2016 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. + */ + import com.example.Note class BootStrap { - def init = { servletContext -> - environments { - test { - new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() - } - } - } - def destroy = { - } + def init = { servletContext -> + environments { + test { + new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() + } + } + } + + def destroy = {} + } diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy index 58ad55977..6da2f620d 100644 --- a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy +++ b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy @@ -1,10 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration class Application extends GrailsAutoConfiguration { - static void main(String[] args) { - GrailsApp.run(Application, args) - } + + static void main(String[] args) { + GrailsApp.run(Application, args) + } + } diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 949f8a568..ae594901e 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2014-2016 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 com.example import org.springframework.restdocs.payload.JsonFieldType @@ -26,121 +42,123 @@ import spock.lang.Specification @Integration @Rollback class ApiDocumentationSpec extends Specification { - @Rule - JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') - protected RequestSpecification documentationSpec + @Rule + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + + protected RequestSpecification documentationSpec + + void setup() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)) + .build() + } - void setup() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)) - .build() - } + void 'test and document get request for /index'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('index-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('message').description('Welcome to Grails!'), + fieldWithPath('environment').description("The running environment"), + fieldWithPath('appversion').description('version of the app that is running'), + fieldWithPath('grailsversion').description('the version of grails used in this project'), + fieldWithPath('appprofile').description('the profile of grails used in this project'), + fieldWithPath('groovyversion').description('the version of groovy used in this project'), + fieldWithPath('jvmversion').description('the version of the jvm used in this project'), + fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + ))) + .when() + .port(8080) + .get('/') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document get request for /index'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('index-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('message').description('Welcome to Grails!'), - fieldWithPath('environment').description("The running environment"), - fieldWithPath('appversion').description('version of the app that is running'), - fieldWithPath('grailsversion').description('the version of grails used in this project'), - fieldWithPath('appprofile').description('the profile of grails used in this project'), - fieldWithPath('groovyversion').description('the version of groovy used in this project'), - fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), - ))) - .when() - .port(8080) - .get('/') - .then() - .assertThat() - .statusCode(is(200)) - } + void 'test and document notes list request'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-list-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('[].class').description('the class of the resource'), + fieldWithPath('[].id').description('the id of the note'), + fieldWithPath('[].title').description('the title of the note'), + fieldWithPath('[].body').description('the body of the note'), + fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document notes list request'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-list-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('[].class').description('the class of the resource'), - fieldWithPath('[].id').description('the id of the note'), - fieldWithPath('[].title').description('the title of the note'), - fieldWithPath('[].body').description('the body of the note'), - fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(8080) - .get('/notes') - .then() - .assertThat() - .statusCode(is(200)) - } + void 'test and document create new note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .contentType(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-create-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + ), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') + .when() + .port(8080) + .post('/notes') + .then() + .assertThat() + .statusCode(is(201)) + } - void 'test and document create new note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .contentType(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-create-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - requestFields( - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') - ), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') - .when() - .port(8080) - .post('/notes') - .then() - .assertThat() - .statusCode(is(201)) - } + void 'test and document getting specific note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('note-get-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes/1') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document getting specific note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('note-get-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(8080) - .get('/notes/1') - .then() - .assertThat() - .statusCode(is(200)) - } } From 4f87f55aaa5d33555976c5c11e5d189421a7e929 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:04:30 +0100 Subject: [PATCH 0298/1059] Fix link to Spring Framework javadoc --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index af4d5bdd4..e917b7b9e 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ ext { springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', - 'http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/', + "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/', 'https://docs.jboss.org/hibernate/stable/validator/api/' ] as String[] From d5a2dff210d92730abafe0058c027264859514c3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:07:19 +0100 Subject: [PATCH 0299/1059] Update the samples to use Spring Boot 1.3.5.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index f4c79f9b2..7b1ed764e 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d30ca93ee..c91372f9d 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 32b12dc4b..824717455 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.3.3.RELEASE + 1.3.5.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 7941829f9..845bf967a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 7683e955d..4e027aa85 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index c9385ea82..b5a9f46c8 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -10,7 +10,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.3.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.5.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 686914071a38c2d25afb230fe88ecdaf111b16bc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:20:19 +0100 Subject: [PATCH 0300/1059] Configure Grails sample to run tests using a random server port See gh-250 --- .../grails-app/conf/application.yml | 2 ++ .../groovy/com/example/ApiDocumentationSpec.groovy | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml index 6d7d00e8a..3fe5b2978 100644 --- a/samples/rest-notes-grails/grails-app/conf/application.yml +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -74,6 +74,8 @@ environments: dataSource: dbCreate: update url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + server: + port: 0 production: dataSource: dbCreate: update diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index ae594901e..3f0da9cae 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -35,8 +35,9 @@ import com.jayway.restassured.specification.RequestSpecification import grails.test.mixin.integration.Integration import grails.transaction.Rollback import org.junit.Rule -import org.springframework.restdocs.JUnitRestDocumentation +import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType +import org.springframework.restdocs.JUnitRestDocumentation import spock.lang.Specification @Integration @@ -46,6 +47,9 @@ class ApiDocumentationSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + @Value('${local.server.port}') + Integer serverPort + protected RequestSpecification documentationSpec void setup() { @@ -75,7 +79,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/') .then() .assertThat() @@ -99,7 +103,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/notes') .then() .assertThat() @@ -130,7 +134,7 @@ class ApiDocumentationSpec extends Specification { ))) .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') .when() - .port(8080) + .port(this.serverPort) .post('/notes') .then() .assertThat() @@ -154,7 +158,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/notes/1') .then() .assertThat() From b804ebbe5ac7f5a5972260e3b363365b338a84de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 11:35:43 +0100 Subject: [PATCH 0301/1059] Add a description to the REST Assured module --- spring-restdocs-restassured/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index b5a9f46c8..ed8fca51a 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs REST Assured' + boolean isPlatformUsingBootOneFour() { def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] bootVersion.startsWith('1.4') From 832f4fdbc3429442629533f5c9cf7d5940557340 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 31 May 2016 10:53:12 +0000 Subject: [PATCH 0302/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4797e2a66..179a34021 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.0.BUILD-SNAPSHOT +version=1.1.1.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 41b7fd63f61ccaa80028ddbc1c5a71f277ed7752 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Jun 2016 13:13:44 +0100 Subject: [PATCH 0303/1059] Fix RestAssuredRestDocumentationConfigurerTests.nextFilterIsCalled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test wasn’t actually verifying that the next filter was being called. This commit corrects that. --- .../RestAssuredRestDocumentationConfigurerTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index d60b85ff3..3417fbbaf 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -35,7 +35,6 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.hasEntry; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -65,8 +64,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Test public void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); - verify(this.filterContext).setValue( - eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), any(Map.class)); + verify(this.filterContext).next(this.requestSpec, this.responseSpec); } @Test From 857525954befbf4350999d4f6364e1ec96697eb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 15:41:22 +0100 Subject: [PATCH 0304/1059] Update samples following the release of 1.1.0.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 7b1ed764e..fa089b3ec 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 85ac7c4e6..3d425282b 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.0.BUILD-SNAPSHOT" + restDocsVersion = "1.1.1.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index c91372f9d..ae6b3f644 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 824717455..eb5b467c3 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.0.BUILD-SNAPSHOT + 1.1.1.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 845bf967a..fdde83881 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 4e027aa85..05c7eea82 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From a4298b29e7f30715c7d8f96334c8d30a02e541a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 13:10:04 +0100 Subject: [PATCH 0305/1059] =?UTF-8?q?Include=20request=20URI=E2=80=99s=20p?= =?UTF-8?q?ort,=20if=20any,=20in=20default=20Host=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, OperationRequestFactory would add a Host header if one did not already exist in the request’s headers, however it did not include the port. This meant that when the request was being made to a non-standard port (a port other than 80 for an HTTP request and 443 for an HTTPS request) the Host header was incorrect. This commit updates OperationRequestFactory to check the URI for a port and, if it has one, include it in the Host header. UriModifyingOperationPreprocessor has also been updated to correctly include the port in the Host header. Closes gh-269 --- .../restdocs/operation/OperationRequestFactory.java | 9 ++++++++- .../restdocs/http/HttpRequestSnippetTests.java | 10 ++++++++++ .../MockMvcRestDocumentationIntegrationTests.java | 2 +- .../preprocess/UriModifyingOperationPreprocessor.java | 10 ++++++---- .../restassured/RestAssuredRequestConverterTests.java | 2 +- .../RestAssuredRestDocumentationIntegrationTests.java | 10 +++++----- .../UriModifyingOperationPreprocessorTests.java | 8 +++++++- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 961987b5b..198011b45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -100,10 +100,17 @@ public OperationRequest createFrom(OperationRequest original, private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { return new HttpHeadersHelper(originalHeaders) - .addIfAbsent(HttpHeaders.HOST, uri.getHost()) + .addIfAbsent(HttpHeaders.HOST, createHostHeader(uri)) .setContentLengthHeader(content).getHeaders(); } + private String createHostHeader(URI uri) { + if (uri.getPort() == -1) { + return uri.getHost(); + } + return uri.getHost() + ":" + uri.getPort(); + } + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { return new HttpHeadersHelper(originalHeaders) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 32e1e6806..df1401ac9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -59,6 +59,16 @@ public void getRequest() throws IOException { .request("http://localhost/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithPort() throws IOException { + this.snippet.expectHttpRequest("get-request") + .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") + .header(HttpHeaders.HOST, "localhost:8080")); + + new HttpRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost:8080/foo").header("Alpha", "a").build()); + } + @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string") diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5d70cfb57..0094abc8a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -375,7 +375,7 @@ public void preprocessedRequest() throws Exception { .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost") + .header("Host", "localhost:8080") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 3c7b109d0..5d1566550 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.restassured.operation.preprocess; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -125,8 +126,10 @@ public OperationRequest preprocess(OperationRequest request) { if (this.scheme != null) { uriBuilder.scheme(this.scheme); } + HttpHeaders modifiedHeaders = modify(request.getHeaders()); if (this.host != null) { uriBuilder.host(this.host); + modifiedHeaders.set(HttpHeaders.HOST, this.host); } if (this.port != null) { if (StringUtils.hasText(this.port)) { @@ -136,10 +139,9 @@ public OperationRequest preprocess(OperationRequest request) { uriBuilder.port(null); } } - HttpHeaders modifiedHeaders = modify(request.getHeaders()); - if (this.host != null) { - modifiedHeaders.set(HttpHeaders.HOST, this.host); - } + URI modifiedUri = uriBuilder.build(true).toUri(); + modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() + + (modifiedUri.getPort() == -1 ? "" : ":" + modifiedUri.getPort())); return this.contentModifyingDelegate.preprocess( new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), request.getContent(), modifiedHeaders, diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 389a878aa..3bf790982 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -142,7 +142,7 @@ public void headers() { assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); assertThat(request.getHeaders().get("Host"), - is(equalTo(Arrays.asList("localhost")))); + is(equalTo(Arrays.asList("localhost:" + this.port)))); } @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 27c055ae0..f81c91f39 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -268,9 +268,9 @@ public void preprocessedRequest() throws Exception { .filter(document("original-request")) .filter(document("preprocessed-request", preprocessRequest(prettyPrint(), - removeHeaders("a", HttpHeaders.HOST, - HttpHeaders.CONTENT_LENGTH), - replacePattern(pattern, "\"<>\"")))) + replacePattern(pattern, "\"<>\""), + modifyUris().removePort(), + removeHeaders("a", HttpHeaders.CONTENT_LENGTH)))) .get("/").then().statusCode(200); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), @@ -279,7 +279,7 @@ public void preprocessedRequest() throws Exception { .header("a", "alpha").header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost") + .header("Host", "localhost:" + this.port) .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); @@ -291,7 +291,7 @@ public void preprocessedRequest() throws Exception { .header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .content(prettyPrinted)))); + .header("Host", "localhost").content(prettyPrinted)))); } @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 8ce532886..d8cffc400 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -63,9 +63,11 @@ public void requestUriSchemeCanBeModified() { public void requestUriHostCanBeModified() { this.preprocessor.host("api.example.com"); OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("http://api.example.com:12345")); + .preprocess(createRequestWithUri("http://api.foo.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com:12345")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com:12345"))); } @Test @@ -75,6 +77,8 @@ public void requestUriPortCanBeModified() { .preprocess(createRequestWithUri("http://api.example.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com:23456")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com:23456"))); } @Test @@ -83,6 +87,8 @@ public void requestUriPortCanBeRemoved() { OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("http://api.example.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com"))); } @Test From 37519398c735dc4dca3c5f04bb0b309cd7b1dc19 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 11:41:57 +0100 Subject: [PATCH 0306/1059] Include customized Host header in curl and HTTPie request snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the curl and HTTPie request snippets always excluded the Host header, relying on the Host header that’s auto-generated by curl and HTTPie instead. This worked well for the most part, but meant that incorrect snippets were generated when the request was being sent with a custom host header that did not match the header that would be auto-generated. This commit updates CliOperationRequest to only filter out the Host header if it’s the same as the header that would be auto-generated by curl or HTTPie. Closes gh-258 --- .../restdocs/cli/CliOperationRequest.java | 49 ++++++++++++++----- .../restdocs/cli/CurlRequestSnippetTests.java | 14 ++++++ .../cli/HttpieRequestSnippetTests.java | 14 ++++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 97d64c256..ce305278a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -17,8 +17,8 @@ package org.springframework.restdocs.cli; import java.net.URI; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -41,20 +41,15 @@ */ final class CliOperationRequest implements OperationRequest { - private static final Set HEADER_FILTERS; - - static { - Set headerFilters = new HashSet<>(); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); - headerFilters.add(new BasicAuthHeaderFilter()); - HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); - } + private final Set headerFilters; private final OperationRequest delegate; CliOperationRequest(OperationRequest delegate) { this.delegate = delegate; + this.headerFilters = new HashSet<>(Arrays.asList( + new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH), + new BasicAuthHeaderFilter(), new HostHeaderFilter(delegate.getUri()))); } Parameters getUniqueParameters() { @@ -121,11 +116,20 @@ public HttpHeaders getHeaders() { } private boolean allowedHeader(Map.Entry> header) { - for (HeaderFilter headerFilter : HEADER_FILTERS) { + for (HeaderFilter headerFilter : this.headerFilters) { if (!headerFilter.allow(header.getKey(), header.getValue())) { return false; } } + if (HttpHeaders.HOST.equalsIgnoreCase(header.getKey())) { + if (!header.getValue().isEmpty()) { + String value = header.getValue().get(0); + if (value.equals(this.delegate.getUri().getHost() + ":" + + this.delegate.getUri().getPort())) { + return false; + } + } + } return true; } @@ -187,4 +191,27 @@ public boolean allow(String name, List value) { } + private static final class HostHeaderFilter implements HeaderFilter { + + private final URI uri; + + private HostHeaderFilter(URI uri) { + this.uri = uri; + } + + @Override + public boolean allow(String name, List value) { + if (value.isEmpty() || this.getImplicitHostHeader().equals(value.get(0))) { + return false; + } + return true; + } + + private String getImplicitHostHeader() { + return this.uri.getHost() + + ((this.uri.getPort() == -1) ? "" : ":" + this.uri.getPort()); + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 86f5a04f7..ba953fed6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -330,4 +330,18 @@ public void customAttributes() throws IOException { .request("http://localhost/foo").build()); } + @Test + public void customHostHeaderIsIncluded() throws IOException { + this.snippet.expectCurlRequest("custom-host-header") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); + new CurlRequestSnippet().document( + operationBuilder("custom-host-header").request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 8f9f48dbc..c3b9a398a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -332,4 +332,18 @@ public void customAttributes() throws IOException { .request("http://localhost/foo").build()); } + @Test + public void customHostHeaderIsIncluded() throws IOException { + this.snippet.expectHttpieRequest("custom-host-header") + .withContents(codeBlock("bash").content( + "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + + " 'Content-Type:application/json' 'a:alpha'")); + new HttpieRequestSnippet().document( + operationBuilder("custom-host-header").request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + } From 8ca1dfa1ad7e84c4abde73db62a398bb1b9bed48 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 17:53:32 +0100 Subject: [PATCH 0307/1059] Improve handling of requests with both parameters and content Previously, if a MockMvc request was made with parameters and body content, the parameters were omitted from the resulting curl, HTTPie and HTTP request snippets. This commit updates the affected snippets to ensure that the parameters are included. The user-provided content is interpreted as indicating that the parameters should be sent in the query string rather than as form-encoded content. Closes gh-239 --- .../restdocs/cli/CurlRequestSnippet.java | 11 ++++++- .../restdocs/cli/HttpieRequestSnippet.java | 14 ++++++-- .../restdocs/http/HttpRequestSnippet.java | 28 +++++++++++++--- .../restdocs/cli/CurlRequestSnippetTests.java | 21 ++++++++++++ .../cli/HttpieRequestSnippetTests.java | 20 ++++++++++++ .../http/HttpRequestSnippetTests.java | 25 +++++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 32 +++++++++++++++++++ 7 files changed, 144 insertions(+), 7 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 73eeb72a3..31127c3c4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -69,7 +69,16 @@ protected Map createModel(Operation operation) { } private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); + OperationRequest request = operation.getRequest(); + if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + return String.format("'%s?%s'", request.getUri(), + request.getParameters().toQueryString()); + } + return String.format("'%s'", request.getUri()); + } + + private boolean includeParametersInUri(OperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; } private String getOptions(Operation operation) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 57673b9c3..4c95c04d1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -89,7 +90,11 @@ private String getOptions(CliOperationRequest request) { return options.toString(); } - private String getUrl(OperationRequest request) { + private String getUrl(CliOperationRequest request) { + if (!request.getUniqueParameters().isEmpty() && includeParametersInUri(request)) { + return String.format("'%s?%s'", request.getUri(), + request.getParameters().toQueryString()); + } return String.format("'%s'", request.getUri()); } @@ -103,11 +108,16 @@ private String getRequestItems(CliOperationRequest request) { } private void writeOptions(CliOperationRequest request, PrintWriter writer) { - if (!request.getParts().isEmpty() || !request.getUniqueParameters().isEmpty()) { + if (!request.getParts().isEmpty() || (!request.getUniqueParameters().isEmpty() + && !includeParametersInUri(request))) { writer.print("--form "); } } + private boolean includeParametersInUri(CliOperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; + } + private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { String credentials = request.getBasicAuthCredentials(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index cd855f7be..c966f137e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -66,14 +66,33 @@ protected HttpRequestSnippet(Map attributes) { protected Map createModel(Operation operation) { Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); - model.put("path", operation.getRequest().getUri().getRawPath() - + (StringUtils.hasText(operation.getRequest().getUri().getRawQuery()) - ? "?" + operation.getRequest().getUri().getRawQuery() : "")); + model.put("path", getPath(operation.getRequest())); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; } + private String getPath(OperationRequest request) { + String path = request.getUri().getRawPath(); + String queryString = request.getUri().getRawQuery(); + if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + if (StringUtils.hasText(queryString)) { + queryString = queryString + "&" + request.getParameters().toQueryString(); + } + else { + queryString = request.getParameters().toQueryString(); + } + } + if (StringUtils.hasText(queryString)) { + path = path + "?" + queryString; + } + return path; + } + + private boolean includeParametersInUri(OperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; + } + private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); @@ -172,7 +191,8 @@ private void writeMultipartEnd(PrintWriter writer) { private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null - && isPutOrPost(request) && !request.getParameters().isEmpty(); + && isPutOrPost(request) && (!request.getParameters().isEmpty() + && !includeParametersInUri(request)); } private Map header(String name, String value) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index ba953fed6..e26ca2176 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -61,6 +61,14 @@ public void getRequest() throws IOException { operationBuilder("get-request").request("http://localhost/foo").build()); } + @Test + public void getRequestWithParameter() throws IOException { + this.snippet.expectCurlRequest("get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); + new CurlRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost/foo").param("a", "alpha").build()); + } + @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( @@ -344,4 +352,17 @@ public void customHostHeaderIsIncluded() throws IOException { .header("a", "alpha").build()); } + @Test + public void postWithContentAndParameters() throws IOException { + this.snippet.expectCurlRequest("post-with-content-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + + "-X POST -d 'Some content'")); + new CurlRequestSnippet() + .document(operationBuilder("post-with-content-and-parameters") + .request("http://localhost/foo").param("a", "alpha") + .method("POST").param("b", "bravo").content("Some content") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index c3b9a398a..9f8164b39 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -62,6 +62,14 @@ public void getRequest() throws IOException { operationBuilder("get-request").request("http://localhost/foo").build()); } + @Test + public void getRequestWithParameter() throws IOException { + this.snippet.expectHttpieRequest("get-request-with-parameter").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); + new HttpieRequestSnippet().document(operationBuilder("get-request-with-parameter") + .request("http://localhost/foo").param("a", "alpha").build()); + } + @Test public void nonGetRequest() throws IOException { this.snippet.expectHttpieRequest("non-get-request").withContents( @@ -346,4 +354,16 @@ public void customHostHeaderIsIncluded() throws IOException { .header("a", "alpha").build()); } + @Test + public void postWithContentAndParameters() throws IOException { + this.snippet.expectHttpieRequest("post-with-content-and-parameters").withContents( + codeBlock("bash").content("$ echo 'Some content' | http POST " + + "'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-with-content-and-parameters") + .request("http://localhost/foo").method("POST") + .param("a", "alpha").param("b", "bravo").content("Some content") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index df1401ac9..6f7057b37 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -59,6 +59,17 @@ public void getRequest() throws IOException { .request("http://localhost/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithParameters() throws IOException { + this.snippet.expectHttpRequest("get-request-with-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") + .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder("get-request-with-parameters") + .request("http://localhost/foo").header("Alpha", "a").param("b", "bravo") + .build()); + } + @Test public void getRequestWithPort() throws IOException { this.snippet.expectHttpRequest("get-request") @@ -103,6 +114,20 @@ public void postRequestWithContent() throws IOException { .request("http://localhost/foo").method("POST").content(content).build()); } + @Test + public void postRequestWithContentAndParameters() throws IOException { + String content = "Hello, world"; + this.snippet.expectHttpRequest("post-request-with-content-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet() + .document(operationBuilder("post-request-with-content-and-parameters") + .request("http://localhost/foo").method("POST") + .param("a", "alpha").content(content).build()); + } + @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 0094abc8a..2b2df2e8e 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -175,6 +175,22 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { + "-H 'Accept: application/json' -d 'a=alpha'")))); } + @Test + public void curlSnippetWithContentAndParametersOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/").param("a", "alpha").accept(MediaType.APPLICATION_JSON) + .content("some content")).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-content-and-parameters")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content-and-parameters/curl-request.adoc"), + is(snippet(asciidoctor()) + .withContents(codeBlock(asciidoctor(), "bash").content("$ curl " + + "'http://localhost:8080/?a=alpha' -i -X POST " + + "-H 'Accept: application/json' -d 'some content'")))); + } + @Test public void httpieSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -207,6 +223,22 @@ public void httpieSnippetWithQueryStringOnPost() throws Exception { + "'Accept:application/json' 'a=alpha'")))); } + @Test + public void httpieSnippetWithContentAndParametersOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/").param("a", "alpha").content("some content") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("httpie-snippet-post-with-content-and-parameters")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-post-with-content-and-parameters/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ echo " + "'some content' | http POST " + + "'http://localhost:8080/?a=alpha' " + + "'Accept:application/json'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From c6f20f213860bb09c06fd57fe7ad9541080b2b2d Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 24 Jun 2016 10:07:42 +0900 Subject: [PATCH 0308/1059] Correct javadoc in LinksSnippet Closes gh-271 --- .../springframework/restdocs/hypermedia/LinksSnippet.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index f0a4cb30e..62543d5b8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -243,8 +243,8 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor } /** - * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link - * extractor and attributes, and its descriptors combined with the given + * Returns a new {@code LinksSnippet} configured with this snippet's link extractor + * and attributes, and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet @@ -254,8 +254,8 @@ public final LinksSnippet and(LinkDescriptor... additionalDescriptors) { } /** - * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link - * extractor and attributes, and its descriptors combined with the given + * Returns a new {@code LinksSnippet} configured with this snippet's link extractor + * and attributes, and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet From a1d5d75c4e4caf16bae5f46c7249b18bd2a7e0db Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 20 Jun 2016 21:29:05 -0500 Subject: [PATCH 0309/1059] Split samples table into three tables grouped by type Closes gh-266 --- docs/src/docs/asciidoc/getting-started.adoc | 45 +++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2a1d161cb..853e2a473 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -10,23 +10,12 @@ This section describes how to get started with Spring REST Docs. If you want to jump straight in, a number of sample applications are available: + [cols="3,2,10"] +.MockMvc |=== | Sample | Build system | Description -| {samples}/rest-assured[REST Assured] -| Gradle -| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. - -| {samples}/rest-notes-grails[Grails] -| Gradle -| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and https://github.com/spockframework/spock[Spock] - -| {samples}/rest-notes-slate[Slate] -| Gradle -| Demonstrates the use of Spring REST Docs with Markdown and - http://github.com/tripit/slate[Slate]. - | {samples}/rest-notes-spring-data-rest[Spring Data REST] | Maven | Demonstrates the creation of a getting started guide and an API guide for a service @@ -37,6 +26,36 @@ If you want to jump straight in, a number of sample applications are available: | Demonstrates the creation of a getting started guide and an API guide for a service implemented using http://projects.spring.io/spring-hateoas/[Spring HATEOAS]. +|=== + + +[cols="3,2,10"] +.REST Assured +|=== +| Sample | Build system | Description + +| {samples}/rest-notes-grails[Grails] +| Gradle +| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and + https://github.com/spockframework/spock[Spock]. + +| {samples}/rest-assured[REST Assured] +| Gradle +| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. + +|=== + + +[cols="3,2,10"] +.Advanced +|=== +| Sample | Build system | Description + +| {samples}/rest-notes-slate[Slate] +| Gradle +| Demonstrates the use of Spring REST Docs with Markdown and + http://github.com/tripit/slate[Slate]. + | {samples}/testng[TestNG] | Gradle | Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. From 42613bf3f9e322515530b1c082362f07b4cb4a9f Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 20 Jun 2016 18:38:50 -0500 Subject: [PATCH 0310/1059] Add README to Grails sample Closes gh-254 Closes gh-268 --- samples/rest-notes-grails/README.md | 95 ++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md index 386292af4..d6c1be30c 100644 --- a/samples/rest-notes-grails/README.md +++ b/samples/rest-notes-grails/README.md @@ -1,2 +1,93 @@ -# grails-spring-restdocs-example -Example of using Spring REST Docs with Grails +# rest-notes-grails + +## Overview + +This is a sample project using Grails 3, Spock, and Spring REST docs. For more +information about the Grails framework please see [grails.org](http://grails.org). + +Grails is built on top of Spring Boot and Gradle so there are a few different ways to +run this project including: + +### Gradle Wrapper (recommended) + +The gradle wrapper allows a project to build without having Gradle installed locally. The +executable file will acquire the version of Gradle and other dependencies recommended for +this project. This is especially important since some versions of Gradle may cause +conflicts with this project. + +On Unix-like platforms, such as Linux and Mac OS X: + +``` +$ ./gradlew run +``` + +On Windows: + +``` +$ gradlew run +``` + +*Please note*, if you are including integration tests in Grails, they will not run as +part of the `gradle test` task. Run them via the build task or individually as +`gradle integrationTest` + +### Gradle Command Line + +Clean the project: + +``` +$ gradle clean +``` + +Build the project: + +``` +$ gradle build +``` + +Run the project: + +``` +$ gradle run +``` + +### Grails Command Line + +Grails applications also have a command line feature useful for code generation and +running projects locally. The command line is accessible by typing `grails` in the +terminal at the root of the project. Please ensure you are running the correct version +of Grails as specified in [gradle.properties](gradle.properties) + +Similar to `gradle clean`, this task destroys the `build` directory and cached assets. + +``` +grails> clean +``` + +The 'test-app' task runs all of the tests for the project. + +``` +grails> test-app +``` + +The `run-app` task is used to run the application locally. By default, the project is +run in development mode including automatic reloading and not caching static assets. It +is not suggested to use this in production. + +``` +grails> run-app +``` + +### Building and Viewing the Docs + +This is an example of the Grails API profile. Therefore, there is no view layer to +return the docs as static assets. The result of running `asciidoctor` or `build` is that +the docs are sent to `/build/asciidoc/`. You can then publish them to a destination of +your choosing using the [gradle github-pages](https://github.com/ajoberstar/gradle-git) +plugin or similar. + +To just generate documentation and not run the application use: + +``` +$ ./gradlew asciidoctor +``` From 417547e61a848c64623ee58bb49900f9ba1b2ece Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jun 2016 11:17:37 +0100 Subject: [PATCH 0311/1059] Support more types of content in RestAssuredRequestConverter Previously, RestAssuredRequestConverter only supported request and request part content that was null, a String or a byte[]. This was particularly problematic for multipart requests as RestAssured performs some internal conversion which meant that, no matter what the when a byte[] was provided it was wrapped in a ByteArrayInputStream meaning that the converter could not read it. The commit improves RestAssuredRequestConverter so that it will now convert File content and InputStream content where the stream supported reset(). Closes gh-252 --- .../RestAssuredRequestConverter.java | 38 ++++++++ .../RestAssuredRequestConverterTests.java | 94 +++++++++++++++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index d2e60f0ce..1673366a0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -16,6 +16,9 @@ package org.springframework.restdocs.restassured; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -35,6 +38,8 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * A converter for creating an {@link OperationRequest} from a REST Assured @@ -64,6 +69,12 @@ private byte[] convertContent(Object content) { else if (content instanceof byte[]) { return (byte[]) content; } + else if (content instanceof File) { + return copyToByteArray((File) content); + } + else if (content instanceof InputStream) { + return copyToByteArray((InputStream) content); + } else if (content == null) { return new byte[0]; } @@ -73,6 +84,33 @@ else if (content == null) { } } + private byte[] copyToByteArray(File file) { + try { + return FileCopyUtils.copyToByteArray(file); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to read content from file " + file, + ex); + } + } + + private byte[] copyToByteArray(InputStream inputStream) { + try { + inputStream.reset(); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot read content from input stream " + + inputStream + " due to reset() failure"); + } + try { + return StreamUtils.copyToByteArray(inputStream); + } + catch (IOException ex) { + throw new IllegalStateException( + "Failed to read content from input stream " + inputStream, ex); + } + } + private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { HttpHeaders httpHeaders = new HttpHeaders(); for (Header header : requestSpec.getHeaders()) { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 3bf790982..9c4f7615e 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -18,6 +18,8 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.net.URI; import java.util.Arrays; import java.util.Collection; @@ -198,25 +200,105 @@ public void objectBody() { } @Test - public void inputStreamBodyIsNotSupported() { + public void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) .port(this.port); requestSpec.post(); - this.thrown.expectMessage( - equalTo("Unsupported request content: java.io.ByteArrayInputStream")); - this.factory.convert((FilterableRequestSpecification) requestSpec); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); } @Test - public void fileBodyIsNotSupported() { + public void fileBody() { RequestSpecification requestSpec = RestAssured.given() .body(new File("src/test/resources/body.txt")).port(this.port); requestSpec.post(); - this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("file"))); + } + + @Test + public void fileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().body(inputStream) + .port(this.port); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + + " due to reset() failure"); this.factory.convert((FilterableRequestSpecification) requestSpec); } + @Test + public void multipartWithByteArrayInputStreamBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithStringBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", "foo"); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithByteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", "file", "foo".getBytes()); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithFileBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart(new File("src/test/resources/body.txt")); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("file"))); + } + + @Test + public void multipartWithFileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("foo", "foo.txt", inputStream); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + + " due to reset() failure"); + this.factory.convert((FilterableRequestSpecification) requestSpec); + } + + @Test + public void multipartWithObjectBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", new ObjectBody("bar")); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("{\"foo\":\"bar\"}"))); + } + /** * Minimal test web application. */ From 0a0136ca5867813cc74984fbc7a77c7ab04cb0cb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 Jul 2016 12:49:17 +0100 Subject: [PATCH 0312/1059] Move spring-webmvc dependency to where it's needed Previously, spring-restdocs-core had a dependency on spring-webmvc however it only used classes from spring-web. This commit moves the spring-webmvc dependency from spring-restdocs-core to spring-restdocs-mockmvc where it's needed. It's now in the runtime scope (rather than compile) as it's not needed at compile time but is needed at runtime by the MockMvc support in spring-test. Closes gh-273 --- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 1bee13f54..4a8417072 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -26,7 +26,7 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'org.springframework:spring-webmvc' + compile 'org.springframework:spring-web' compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index b93fba78d..d8a0529cb 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -4,6 +4,7 @@ dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') optional 'junit:junit' + runtime 'org.springframework:spring-webmvc' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' From a3159b6f1c851044e1c55ec6888c6e880a23b5f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 Jul 2016 15:37:22 +0100 Subject: [PATCH 0313/1059] Update README to link to restdocsext-jersey third-party extension --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50f3ebe86..c9b178feb 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ There are several that you can contribute to Spring REST Docs: | Name | Description | | ---- | ----------- | | [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | +| [restdocsext-jersey][19] | Enables Spring REST Docs to be used with [Jersey's test framework][20] | ## Licence @@ -60,4 +61,6 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [15]: http://stackoverflow.com/tags/spring-restdocs [16]: https://gitter.im/spring-projects/spring-restdocs [17]: https://github.com/ePages-de/restdocs-wiremock -[18]: http://wiremock.org/ \ No newline at end of file +[18]: http://wiremock.org/ +[19]: https://github.com/RESTDocsEXT/restdocsext-jersey +[20]: https://jersey.java.net/documentation/latest/test-framework.html \ No newline at end of file From 4f00cebaae10dfc9f228090e1cafc8ce35e16eeb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 Jul 2016 10:47:14 +0100 Subject: [PATCH 0314/1059] Adopt the new CLA signing process --- CONTRIBUTING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b609d53c..4d7046ebc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,7 @@ spring-code-of-conduct@pivotal.io. Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's license agreement (CLA)][2]. Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept -your contributions, and you will get an author credit if we do. Please use "Andy -Wilkinson" in the project lead field when you complete the form. +your contributions, and you will get an author credit if we do. ## Code conventions and housekeeping @@ -61,5 +60,5 @@ The project can then be imported into Eclipse using `File -> Import…` and then `General -> Existing Projects into Workspace`. [1]: CODE_OF_CONDUCT.md -[2]: https://support.springsource.com/spring_committer_signup +[2]: https://cla.pivotal.io/sign/spring [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file From 19d7337e264cdfc6e526d37dac81617194fc22c6 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 22 Jul 2016 08:12:04 +0900 Subject: [PATCH 0315/1059] Remove unnecessary Host header set in UriModifyingOperationPreprocessor Closes gh-280 --- .../preprocess/UriModifyingOperationPreprocessor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 5d1566550..1a4798147 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -126,10 +126,8 @@ public OperationRequest preprocess(OperationRequest request) { if (this.scheme != null) { uriBuilder.scheme(this.scheme); } - HttpHeaders modifiedHeaders = modify(request.getHeaders()); if (this.host != null) { uriBuilder.host(this.host); - modifiedHeaders.set(HttpHeaders.HOST, this.host); } if (this.port != null) { if (StringUtils.hasText(this.port)) { @@ -140,6 +138,7 @@ public OperationRequest preprocess(OperationRequest request) { } } URI modifiedUri = uriBuilder.build(true).toUri(); + HttpHeaders modifiedHeaders = modify(request.getHeaders()); modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() + (modifiedUri.getPort() == -1 ? "" : ":" + modifiedUri.getPort())); return this.contentModifyingDelegate.preprocess( From eb46eaa3c55b5b6099aed9c5e406da602ee2f386 Mon Sep 17 00:00:00 2001 From: mle Date: Thu, 21 Jul 2016 20:06:15 +0200 Subject: [PATCH 0316/1059] Improve documention on how to use .adoc snippets Closes gh-279 --- docs/src/docs/asciidoc/getting-started.adoc | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 853e2a473..9bd7907a4 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -419,7 +419,26 @@ that can be produced by Spring REST Docs. [[getting-started-using-the-snippets]] === Using the snippets -The generated snippets can be included in your documentation using the +Before using the generated snippets from above, an ```\*.adoc``` source file must be manually created. +Its filename does not matter for either of the two build tools' Asciidoctor plugins mentioned +above and will also be the name of the finally generated ```*.html``` file. +For the exact locations refer to the following table. + +[cols="2,5,8"] +|=== +| Build tool | Source files | Generated files + +| Maven +| ```src/main/asciidoc/*.adoc``` +| ```${project.build.directory}/generated-docs/*.html``` + +| Gradle +| ```src/docs/asciidoc/*.adoc``` +| ```$buildDir/asciidoc/*.html``` + +|=== + +The generated snippets can then be included in the manually created Asciidoctor file from above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: From 9c97c8b9bf17a8ebfa50ca38450e356da6663a92 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 10:45:14 +0100 Subject: [PATCH 0317/1059] Polish "Improve documention on how to use .adoc snippets" See gh-279 --- docs/src/docs/asciidoc/getting-started.adoc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9bd7907a4..0dd076206 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -419,26 +419,27 @@ that can be produced by Spring REST Docs. [[getting-started-using-the-snippets]] === Using the snippets -Before using the generated snippets from above, an ```\*.adoc``` source file must be manually created. -Its filename does not matter for either of the two build tools' Asciidoctor plugins mentioned -above and will also be the name of the finally generated ```*.html``` file. -For the exact locations refer to the following table. +Before using the generated snippets, a `.adoc` source file must be created. You can name +the file whatever you like as long as it has a `.adoc` suffix. The result HTML file will +have the same name but with a `.html` suffix. The default location of the source files and +the resulting HTML files depends on whether you are using Maven or Gradle: [cols="2,5,8"] |=== | Build tool | Source files | Generated files | Maven -| ```src/main/asciidoc/*.adoc``` -| ```${project.build.directory}/generated-docs/*.html``` +| `src/main/asciidoc/*.adoc` +| `target/generated-docs/*.html` | Gradle -| ```src/docs/asciidoc/*.adoc``` -| ```$buildDir/asciidoc/*.html``` +| `src/docs/asciidoc/*.adoc` +| `build/asciidoc/html5/*.html` |=== -The generated snippets can then be included in the manually created Asciidoctor file from above using the +The generated snippets can then be included in the manually created Asciidoctor file from +above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: From 86c2310b90d0763b2d0dc93a01083d05fc474372 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 11:44:43 +0100 Subject: [PATCH 0318/1059] Improve message when fields cannot be documented due to empty body Closes gh-278 --- .../restdocs/payload/AbstractFieldsSnippet.java | 13 +++++++++++-- .../payload/RequestFieldsSnippetFailureTests.java | 11 +++++++++++ .../payload/ResponseFieldsSnippetFailureTests.java | 11 +++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index fcc67c360..393dd8fd8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -43,6 +43,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final boolean ignoreUndocumentedFields; + private final String type; + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given @@ -88,6 +90,7 @@ protected AbstractFieldsSnippet(String type, List descriptors, } this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; + this.type = type; } @Override @@ -116,13 +119,19 @@ protected Map createModel(Operation operation) { private ContentHandler getContentHandler(Operation operation) { MediaType contentType = getContentType(operation); ContentHandler contentHandler; + try { + byte[] content = getContent(operation); + if (content.length == 0) { + throw new SnippetException("Cannot document " + this.type + + " fields as the " + this.type + " body is empty"); + } if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(getContent(operation)); + contentHandler = new XmlContentHandler(content); } else { - contentHandler = new JsonContentHandler(getContent(operation)); + contentHandler = new JsonContentHandler(content); } } catch (IOException ex) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index eeab98cfa..a6dceaf0f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -99,6 +99,17 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .content("{ \"a\": { \"c\": 5 }}").build()); } + @Test + public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("Cannot document request fields as the request body is empty")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("no-request-body", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 98a31d45a..e3385a03a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -50,6 +50,17 @@ public class ResponseFieldsSnippetFailureTests { @Rule public ExpectedException thrown = ExpectedException.none(); + @Test + public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("Cannot document response fields as the response body is empty")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("no-response-body", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + @Test public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); From 78318775e0a24ee901e959531fd0fbeb6a083b4b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 13:20:44 +0100 Subject: [PATCH 0319/1059] Cause a failure when field's documented and actual types do not match Closes gh-276 --- .../payload/AbstractFieldsSnippet.java | 11 +++++- .../restdocs/payload/ContentHandler.java | 8 ++-- .../FieldTypesDoNotMatchException.java | 39 +++++++++++++++++++ .../restdocs/payload/JsonContentHandler.java | 25 +++++++++--- .../restdocs/payload/XmlContentHandler.java | 15 ++++--- .../RequestFieldsSnippetFailureTests.java | 27 +++++++++++++ .../payload/RequestFieldsSnippetTests.java | 28 +++++++++++++ .../ResponseFieldsSnippetFailureTests.java | 28 ++++++++++++- .../payload/ResponseFieldsSnippetTests.java | 27 +++++++++++++ 9 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 393dd8fd8..a21bf524c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -100,8 +100,15 @@ protected Map createModel(Operation operation) { validateFieldDocumentation(contentHandler); for (FieldDescriptor descriptor : this.fieldDescriptors) { - if (descriptor.getType() == null) { - descriptor.type(contentHandler.determineFieldType(descriptor.getPath())); + try { + descriptor.type(contentHandler.determineFieldType(descriptor)); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the " + + "payload. Please provide a type using " + + "FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java index ce99ff00f..45865f4ba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -50,12 +50,12 @@ interface ContentHandler { String getUndocumentedContent(List fieldDescriptors); /** - * Returns the type of the field with the given {@code path} based on the content of - * the payload. + * Returns the type of the field that is described by the given + * {@code fieldDescriptor} based on the content of the payload. * - * @param path the field path + * @param fieldDescriptor the field descriptor * @return the type of the field */ - Object determineFieldType(String path); + Object determineFieldType(FieldDescriptor fieldDescriptor); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java new file mode 100644 index 000000000..cfb8667a9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2016 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; + +/** + * A {@code FieldTypesDoNotMatchException} is thrown when the documented and actual types + * of a field do not match. + * + * @author Andy Wilkinson + */ +class FieldTypesDoNotMatchException extends RuntimeException { + + /** + * Creates a new {@code FieldTypesDoNotMatchException} for the field described by the + * given {@code fieldDescriptor} that has the given {@code actualType}. + * + * @param fieldDescriptor the field + * @param actualType the actual type of the field + */ + FieldTypesDoNotMatchException(FieldDescriptor fieldDescriptor, Object actualType) { + super("The documented type of the field '" + fieldDescriptor.getPath() + "' is " + + fieldDescriptor.getType() + " but the actual type is " + actualType); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 779b75fbc..f1fff9ce4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -34,6 +34,8 @@ class JsonContentHandler implements ContentHandler { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); + private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); + private final ObjectMapper objectMapper = new ObjectMapper() .enable(SerializationFeature.INDENT_OUTPUT); @@ -93,15 +95,26 @@ private boolean isEmpty(Object object) { } @Override - public Object determineFieldType(String path) { + public Object determineFieldType(FieldDescriptor fieldDescriptor) { + if (fieldDescriptor.getType() == null) { + return this.fieldTypeResolver.resolveFieldType(fieldDescriptor.getPath(), + readContent()); + } + if (!(fieldDescriptor.getType() instanceof JsonFieldType)) { + return fieldDescriptor.getType(); + } + JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType(); try { - return new JsonFieldTypeResolver().resolveFieldType(path, readContent()); + JsonFieldType actualFieldType = this.fieldTypeResolver + .resolveFieldType(fieldDescriptor.getPath(), readContent()); + if (descriptorFieldType == JsonFieldType.VARIES + || descriptorFieldType == actualFieldType) { + return descriptorFieldType; + } + throw new FieldTypesDoNotMatchException(fieldDescriptor, actualFieldType); } catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" + path + "' as" - + " it is not present in the payload. Please provide a type using" - + " FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + return fieldDescriptor.getType(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 67e2763d5..2e8f99e74 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -153,15 +153,14 @@ private String prettyPrint(Document document) { } @Override - public Object determineFieldType(String path) { - try { - return new JsonFieldTypeResolver().resolveFieldType(path, readPayload()); + public Object determineFieldType(FieldDescriptor fieldDescriptor) { + if (fieldDescriptor.getType() != null) { + return fieldDescriptor.getType(); } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" + path + "' as" - + " it is not present in the payload. Please provide a type using" - + " FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + else { + throw new FieldTypeRequiredException("The type of a field in an XML payload " + + "cannot be determined automatically. Please provide a type using " + + "FieldDescriptor.type(Object type)"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index a6dceaf0f..f27633c2f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -110,6 +110,33 @@ public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { .build()); } + @Test + public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" + + " Object but the actual type is Number")); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{ \"a\": 5 }").build()); + } + + @Test + public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" + + " Object but the actual type is Varies")); + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") + .build()); + } + @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index fa23f3467..32e34be9e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -177,6 +177,34 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void fieldWithExplictExactlyMatchingType() throws IOException { + this.snippet + .expectRequestFields("request-field-with-explicit-exactly-matching-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Number`", "one")); + + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder( + "request-field-with-explicit-exactly-matching-type") + .request("http://localhost") + .content("{\"a\": 5 }").build()); + } + + @Test + public void fieldWithExplictVariesType() throws IOException { + this.snippet.expectRequestFields("request-field-with-explicit-varies-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Varies`", "one")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type(JsonFieldType.VARIES))).document( + operationBuilder("request-field-with-explicit-varies-type") + .request("http://localhost").content("{\"a\": 5 }") + .build()); + } + @Test public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index e3385a03a..bfe50ad0c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -57,8 +57,32 @@ public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { equalTo("Cannot document response fields as the response body is empty")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) .document(new OperationBuilder("no-response-body", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + this.snippet.getOutputDirectory()).build()); + } + + @Test + public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" + + " Object but the actual type is Number")); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()).response() + .content("{ \"a\": 5 }}").build()); + } + + @Test + public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" + + " Object but the actual type is Varies")); + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()).response() + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index db13d9424..e0525888d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -185,6 +185,33 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void fieldWithExplictExactlyMatchingType() throws IOException { + this.snippet + .expectResponseFields( + "response-field-with-explicit-exactly-matching-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Number`", "one")); + + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder( + "response-field-with-explicit-exactly-matching-type") + .response().content("{\"a\": 5 }").build()); + } + + @Test + public void fieldWithExplictVariesType() throws IOException { + this.snippet.expectResponseFields("response-field-with-explicit-varies-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Varies`", "one")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type(JsonFieldType.VARIES))).document( + operationBuilder("response-field-with-explicit-varies-type") + .response().content("{\"a\": 5 }").build()); + } + @Test public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") From 3dceea8bb5ce8454ce86a0c14cdc0332ee64270f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:19:13 +0100 Subject: [PATCH 0320/1059] Document the tableCellContent Mustache lambda Closes gh-251 --- .../asciidoc/working-with-asciidoctor.adoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index ce166410b..b3981dfeb 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -69,6 +69,24 @@ The title of a table can be specified using a line prefixed by a `.`: +[[working-with-asciidoctor-customizing-tables-title]] +==== Avoiding table formatting problems + +Asciidoctor uses the `|` character to delimit cells in a table. This can cause problems +if you want a `|` to appear in a cell's contents. The problem can be avoided by +escaping the `|` with a backslash, i.e. by using `\|` rather than `|`. + +All of the default Asciidoctor snippet templates perform this escaping automatically +use a Mustache lamba named `tableCellContent`. If you write your own custom templates +you may want to use this lamba. For example, to escape `|` characters +in a cell that contains the value of a `description` attribute: + +---- +| {{#tableCellContent}}{{description}}{{/tableCellContent}} +---- + + + ==== Further reading Refer to the http://asciidoctor.org/docs/user-manual/#tables[Tables section of From 3ec945fe58845893847fd1e931f00ab0279f36b8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:30:09 +0100 Subject: [PATCH 0321/1059] Make it clearer how to use RestDocumentationResultHandler.document Closes gh-255 --- .../mockmvc/RestDocumentationResultHandler.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 5a85fc5c2..1522fa2d0 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.Assert; @@ -60,7 +61,8 @@ public void handle(MvcResult result) throws Exception { * * @param snippets the snippets to add * @return this {@code RestDocumentationResultHandler} - * @deprecated since 1.1 in favor of {@link #document(Snippet...)} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} and passing the + * return value into {@link ResultActions#andDo(ResultHandler)} */ @Deprecated public RestDocumentationResultHandler snippets(Snippet... snippets) { @@ -69,8 +71,17 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { } /** - * Creates a new {@link RestDocumentationResultHandler} that will produce - * documentation using the given {@code snippets}. + * Creates a new {@link RestDocumentationResultHandler} to be passed into + * {@link ResultActions#andDo(ResultHandler)} that will produce documentation using + * the given {@code snippets}. For example: + * + *
            +	 * this.mockMvc.perform(MockMvcRequestBuilders.get("/search"))
            +	 *     .andExpect(status().isOk())
            +	 *     .andDo(this.documentationHandler.document(responseFields(
            +	 *          fieldWithPath("page").description("The requested Page")
            +	 *     ));
            +	 * 
            * * @param snippets the snippets * @return the new result handler From 6c6054178ea1ee00b42933b5cf87b9eca1f600d7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:41:45 +0100 Subject: [PATCH 0322/1059] Upgrade tests and samples to use Spring Boot 1.3.6.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index fa089b3ec..a02386b96 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index ae6b3f644..e7cd576b8 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index eb5b467c3..8c17bc420 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.3.5.RELEASE + 1.3.6.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index fdde83881..28211f126 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 05c7eea82..b962681ec 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index ed8fca51a..a861c7e51 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -12,7 +12,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.5.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 8a91f170545bcf4eb6a9745d50ad0a8d1e45239e Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Mon, 25 Jul 2016 09:24:28 +0000 Subject: [PATCH 0323/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 179a34021..af18773ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.1.BUILD-SNAPSHOT +version=1.1.2.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From d9192380230461573e3715e6ff60104af21d62e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 11:06:49 +0100 Subject: [PATCH 0324/1059] Update the samples following the release of 1.1.1.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index a02386b96..6508143d2 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 3d425282b..74a25c404 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.1.BUILD-SNAPSHOT" + restDocsVersion = "1.1.2.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index e7cd576b8..d0a057657 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 8c17bc420..d01f29d78 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.1.BUILD-SNAPSHOT + 1.1.2.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 28211f126..b1a9aa41f 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index b962681ec..723b2430a 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From 2db7ab5e711d7bd8afe37f0bf8aeb8b30809da1f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 11:08:36 +0100 Subject: [PATCH 0325/1059] Start work on 1.2.0 --- gradle.properties | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index af18773ba..0f5d13f0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.2.BUILD-SNAPSHOT +version=1.2.0.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 6508143d2..4d5bb5237 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 74a25c404..535ba3150 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.2.BUILD-SNAPSHOT" + restDocsVersion = "1.2.0.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d0a057657..91aea3b80 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index d01f29d78..21699808e 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.2.BUILD-SNAPSHOT + 1.2.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index b1a9aa41f..1be4c82f1 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 723b2430a..f1e791c84 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From 67d31fd7d9ec1426eab7c3a8ab37de7c7ffa5bd6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 14:29:52 +0100 Subject: [PATCH 0326/1059] Upgrade to Jacoco 0.7.7.201606060606 Closes gh-282 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e917b7b9e..498ea2f65 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ subprojects { dependency 'org.hibernate:hibernate-validator:5.2.2.Final' dependency 'org.mockito:mockito-core:1.10.19' dependency 'org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE' - dependency 'org.jacoco:org.jacoco.agent:0.7.5.201505241946' + dependency 'org.jacoco:org.jacoco.agent:0.7.7.201606060606' } } From 5c74bef194f7ba8894ed1fe96d55b8dfe51174d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 14:36:25 +0100 Subject: [PATCH 0327/1059] Upgrade to Gradle 2.14.1 Closes gh-283 --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Mon, 25 Jul 2016 14:53:54 +0100 Subject: [PATCH 0328/1059] Configure sonar.branch --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 498ea2f65..3881aab5d 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ apply plugin: 'org.sonarqube' sonarqube { properties { + property 'sonar.branch', '1.1.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' From 10e421a105f4a1f818b27866dcf64dbce7be83a6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 15:39:12 +0100 Subject: [PATCH 0329/1059] Upgrade the samples that use Gradle to 2.14.1 as well See gh-283 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-assured/gradlew | 46 ++++++++++-------- samples/rest-assured/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-slate/gradlew | 46 ++++++++++-------- samples/rest-notes-slate/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-spring-hateoas/gradlew | 46 ++++++++++-------- samples/rest-notes-spring-hateoas/gradlew.bat | 8 +-- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/testng/gradlew | 46 ++++++++++-------- samples/testng/gradlew.bat | 8 +-- 16 files changed, 124 insertions(+), 108 deletions(-) diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 zcmaJ_2{=@3`yazZ*_W|38vDM7tSS2*QuZxlDND8xF`-n}>Le1PNJ_RD^oqoo!Prt+ zlAY{{O4j%vGv4~<`>yMMuIpUqKKJ~7_x3!`eV%8oJ)!Q%qh_)+rKULqfzZ)GX!O4u z-C+`8+4D&9{4}IzhZy;}CP#V#l2l_P z0mB3hhLTm+Xr{@k0N4|<3InHOJ3!V+F%3YO_AH?yJ4Wx)KpVGaNfA z#;O7cV!e`)V?ot5M{7AF5v(MKFdQmESLiY?kS%&6A!MS>nCjx{rh_ZOdp`amQ&l?076d~96&$-X*_=`N)WdI1q^p#8+T1JUV?qGEQ)ab)`_iz*yw^qIdzIM* zty0Ht&eOqX@S;n`cdRR5r$tf4Mw3FY>O=+v%pSg$|6+i^Bxf*vGcPaB8IGJ}hd-J) z3V$>in<%$j7*}R<1)Xw{=D?{DYp6_mhTRF7!W91ijxwpw+bRo|m#I(-?IDM<)xy$$ zrRzmZNJt6Aw}qOWGlcf8oy#NI2rE0q%2j{HEW5NwW=3d0drt@+@I_gkv$q)Rt6qF~ zWoLD9Cv&NFGGinnYYJ!Qb-T>GV(wFt=knc_yqANLdBRm@Hlt{Fx|#<_;%V(jPX|eM zZ|xP&>YR?}bX)3xb2mqS_yIv>`(Tl&Te82Iw+tUBr`I2F z>hk3eEF^qqhc*9D?M5nI%}9zww$NwyHg=cYW~Tlk;-G_o8Fr5yZ~k`KNZT`dWY+Mi zwG2f5>d90_?k0J(m6PPe#I#t%Z%SLj7T_wY|_>Jy)AH0+2EJK4Z~oc zvHlv`OG7g7hOM?Ekxy<|_A~Y9{VcAYMmTUkR=!bvCmO5G)0IC7$$|FVq0Z)vxVB(Z zOG6lr31{*i+*S+KZd&e{`Ueq7(28NMdh||hYHiBddrs@E8k+0u+Kw7`Bm>vRtl{lj zv_Wdxn+gd*O7Df3k@^~6>?4AhI;Seq88+Kok@gES-Z!x{YN=L-#bn&sFo~=K*yH?9 zn0Z!dcicB%Wn;F<)5cEcu*XB$;8U{hKUu;b4C;ome=>~puS?8WR+Q)sjF&Q;6RE)tDEv3zTgd{+X;$dhYpb?>XWy`7|qLNnT})xy4j>v6ef z*g4B_VwiryL&*SqAi`0>uN^(JTp@n^2u^Idjr;g@F8xhxVt4EICvJ<$5XCDlcz?L- z&50xh;bm(MSGF&5?aOe5pJ(p8;l%fQ?ig^4CQmpjTsb_-IwNPeGA#Z^Ow9G{N;F*C zGSIIUTgoM7-cr?BmFSDUFAa0(t7M3J{7fTtTwL|iw%D z*FA?-Jo)?*h8TfoTScZ}0+X+aLzSz+pURJh)V@|AJX0w?|DmU1DyDzpt50L#+K-AX zKhFgTv=_F&QwQItA0E0aQ_oa)D{$N|x(p7J>BNRUGx(vjki-{O+$`^h zE+itIVDHahO59X7rqPS_@v4p}mzM*0sanj*TZql~x-xZG%_|Wf0ul9YW?3*?5d7zc zNos{lg?YFJ@OkUlD$e(Fsg3vNsvy`Q56y~}`Hy|IR!r-+RCDP?`&3j+kEImL^p_*j zrf+|#cygOj#I!4}V=H@=wp9k#Dc$MKOlLpqRGG9Y_@>N!(i8pE`DC&|%L9S5-#hW2 zd~DE0Y{Yo_!o>Q3R#B;FSH_gqgN#oiY;d$eR(L61(Pdr6{$*@Jk^BpRr1LIQ_4k#x z>B5yXX>)9AdGHZ<>1jqsT$=uj*t!Hyo6q`#5`&cx#s-$Tj8{*_wl&HED^{f74Sn+Q z_9OE{Q$mt4PsY(T>p6*|zU$OG&&E%!W8!3)2ShtP37tB9!6|Whn@Qry6J>EehK(zn z56-^(FpyJbSNhNS#SKjOV;hNS_Oor~e&zD)PtTOTI@q9O(ERH~a%R)$ayVo7N>;*i zy(_jumsF0OU{oIR_#WRyNV0W5dbsUjbt2j)!kJAz(YY@dh2A+ASC}rJqlb?iksKDx zY<9FVdX(bk)gQpUP{+#L(AnOGYipdopgHofmr&2UU{YN&@AXBk?5@xRktoF-;~1_k zE-_~ra9-e5y!S)i0D9f_ogDYH?+OR#kvO~dvvmzGYrdD)H2OvrTLo_J{^V&8L)EYO z<=A!cAvR)Do})1vGzahGj@6V|23zvl6Sz|RYl4U#-B_6M8VsJ{p{GIMVQ#scl92u&G{hTSNRRWKBGE}mGrux>!S{pKEKhTA5qGVxWnIaJDiWQptD>0;ZkE?4vMXn2pVym+NB(O3rX;eA zU=JU`-T&_Yv4Sx|mE%;__q)%=sirjj{J$T7jeJ|q&PLpNRC|LqICMbksNO}b*Q)hw zf{#adH`QsMM+A!1BsH6CGaT*RP4M1eIK5LEJ(LoJFr@WZLRD_fZAeGczgbh*p^H?w zk+!UdrrmhZ0a^97ldAPVz_*tZ z!;CHyE=s2qPk+E>3r%8jcdwzVm-6Y6tD4U~+BKdjYu%V*S+6XX35#rP(fvojxbbb_ zadpHX&pf@BEgr|1CdR|VZJHV6lUx7{Okzh`(I%&ceVKFJ5q?3cv1iK8DY*!)$k~sF z8;|XZ3J z*;HCs<%x{GuV&|YX*R0#D~fMUxG#>OBx>eIN1hFztHD{m*Th}Qa{`qSMwI^yJ9(>* zK1vTGOR1`Gda_ic`|A){%C{KWKN7^AIk10BNV2t|W8C|=PF{cKdIUU^Ab>?zNfjw^ zVz-I8tVFfBoQYYRaj$t-+mxhar&L>qc@^wG0$2i>?bkb=hJ(x$2oU3jM7P&7HSu$t z7|*d)IRDCRgU4>j$>}F->5GoAM4`mTnVR_A6;mcY&vD!0V&)P@OSWX}SV51)=Oj(` zie3jo4)s<6L}|+`n4l)emQS_7r=Zq_Sjm2y^!|}l!0%y$)>l7;kDvcm`107I#Iu8? z?AvoeHYQul4S)6wFvwm^K!m##&7OcMa=!s(-IEkuJ=30oVa|fLIM5R+40xbw$e1IZ zJ5aL3?=!nEG5Fr4BJs4je0E!CA&?vF;K_-^;~*k1-~4)L$&}{MBh+MRI$WKaY0s5l zWShfX7zFZw5ul2P1A%Gu7>lT9FfwZM>IM~AT8sO*Z=*Z$9od3}n^r5JVhWu-C36z=?ef8V&kWA3vb8z3Z7ri9F z+s%m?EqlCAxnAkCAI%rciG8xftL}Z>_CKe|#WSd;ThpS3K!^t+5DijXX`L{Dp$Cr1 ztuv%%+e?~x7iY@>)|L&tkr2SVM?m<`Ec)`FbB7AbLH+kk31|MeKH4>F9v(dS(^Se_TLQpe+iI1M&mxiJJ=oN=lTB{aDc@F;g8N3B4z?XrbCIu3PrZ{rg2{dR08kC{HcmRz}nxy3p zP~mu?hv-Q5Rq6NvyJiMbFdd9826sU21~?-qx%^BZ7y)GAkR;&WzY8D`c?u`nEF`N- zI0>?+!_B^k18K$ht##Ug%Fn zl1>9JN=n_RuSWO-IDLe;!DUQglp45-KSt)g26+fw5^oVNM9St<7i&Zwm@*bTJW!UE z(_-H#EnrF@57-6%3%9XU5Xf0@%Bxa1`DH@_FLN^j`2=B-<+~mxhk58A5L0FdM1TSe zu_b{^ZJ7YN7IO9fv{?{HIdAM-ffZK~z#atdEvLU-6#@ZM{c~YsYg~crEuy4#4wSX< zLQ!r&dkY`=eGz;I+<@^S9-zye7tm;BAnQ$Ad7(!vu4r^2wFEru>7`g19v?&@s1v#b^^z`p2 zbn`-kDo8aet7QI@qHYi8^gLLvE1+jh3UW^+2o?hRy7wmv)jM7&yawQVXGjWJ2JRC_ z3l86FFlFbW4m8}^j{ E2en2;ga7~l delta 5699 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 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 72a999e4b..92953a8ce 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:16 GMT 2016 +#Mon Jul 25 15:38:29 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 9d82f7891..27309d923 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-assured/gradlew.bat +++ b/samples/rest-assured/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar index e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5494 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 MSIUeM4P@v40p5%%-2eap delta 5701 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 bKi@x~80mdC-3;tBodo3HFj3mK9J%^G#jsYK diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties index 90a0f04cd..01d6474a9 100644 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 15 14:25:16 BST 2015 +#Mon Jul 25 15:38:07 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew index 97fac783e..27309d923 100755 --- a/samples/rest-notes-slate/gradlew +++ b/samples/rest-notes-slate/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-notes-slate/gradlew.bat +++ b/samples/rest-notes-slate/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 347aa08f3..403d06ec6 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:28 GMT 2016 +#Mon Jul 25 15:38:17 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 9d82f7891..27309d923 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-notes-spring-hateoas/gradlew.bat +++ b/samples/rest-notes-spring-hateoas/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 9332fb8f6..8884c2783 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:46 GMT 2016 +#Mon Jul 25 15:38:37 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 9d82f7891..27309d923 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/testng/gradlew.bat +++ b/samples/testng/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From 65571d98dd0fc9416d28885cce536a24f2c46cf4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 18:01:55 +0100 Subject: [PATCH 0330/1059] Revert "Upgrade to Gradle 2.14.1" This reverts commit 5c74bef194f7ba8894ed1fe96d55b8dfe51174d8 and commit 10e421a105f4a1f818b27866dcf64dbce7be83a6. See gh-283 --- gradle/wrapper/gradle-wrapper.jar | Bin 53639 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-assured/gradlew | 46 ++++++++---------- samples/rest-assured/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53638 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-slate/gradlew | 46 ++++++++---------- samples/rest-notes-slate/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-spring-hateoas/gradlew | 46 ++++++++---------- samples/rest-notes-spring-hateoas/gradlew.bat | 8 +-- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/testng/gradlew | 46 ++++++++---------- samples/testng/gradlew.bat | 8 +-- 18 files changed, 110 insertions(+), 126 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c6137b87896c8f70315ae454e00a969ef5f6019..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 1839 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 92953a8ce..72a999e4b 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:29 BST 2016 +#Mon Feb 15 17:16:16 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 27309d923..9d82f7891 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-assured/gradlew.bat +++ b/samples/rest-assured/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 100644 GIT binary patch delta 5701 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 bKi@x~80mdC-3;tBodo3HFj3mK9J%^G#jsYK delta 5494 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 MSIUeM4P@v40p5%%-2eap diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties index 01d6474a9..90a0f04cd 100644 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:07 BST 2016 +#Thu Oct 15 14:25:16 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew index 27309d923..97fac783e 100755 --- a/samples/rest-notes-slate/gradlew +++ b/samples/rest-notes-slate/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-notes-slate/gradlew.bat +++ b/samples/rest-notes-slate/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 5699 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 403d06ec6..347aa08f3 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:17 BST 2016 +#Mon Feb 15 17:16:28 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 27309d923..9d82f7891 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-notes-spring-hateoas/gradlew.bat +++ b/samples/rest-notes-spring-hateoas/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 5699 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 8884c2783..9332fb8f6 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:37 BST 2016 +#Mon Feb 15 17:16:46 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 27309d923..9d82f7891 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/testng/gradlew.bat +++ b/samples/testng/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From f2717363f034d898f3a33d0a964a24ff0638da8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jul 2016 12:09:20 +0100 Subject: [PATCH 0331/1059] Raise the minimum Spring Framework version to 4.3.x Closes gh-284 --- build.gradle | 4 ++-- docs/src/docs/asciidoc/getting-started.adoc | 2 +- samples/rest-assured/build.gradle | 1 + samples/rest-notes-grails/build.gradle | 1 + samples/rest-notes-slate/build.gradle | 1 + samples/rest-notes-spring-data-rest/pom.xml | 1 + samples/rest-notes-spring-hateoas/build.gradle | 1 + samples/testng/build.gradle | 2 ++ .../restdocs/operation/AbstractOperationMessage.java | 2 +- .../operation/preprocess/PatternReplacingContentModifier.java | 4 ++-- .../preprocess/UriModifyingOperationPreprocessor.java | 4 ++-- 11 files changed, 15 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 498ea2f65..330e83554 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.2.5.RELEASE' + springVersion = '4.3.1.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", @@ -60,7 +60,7 @@ subprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" } dependencies { - dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.5.5' dependency 'com.jayway.restassured:rest-assured:2.8.0' dependency 'com.samskivert:jmustache:1.12' dependency 'commons-codec:commons-codec:1.10' diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0dd076206..9b0040ab3 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -68,7 +68,7 @@ If you want to jump straight in, a number of sample applications are available: Spring REST Docs has the following minimum requirements: - Java 7 -- Spring Framework 4.2 +- Spring Framework 4.3 Additionally, the `spring-restdocs-restassured` module has the following minimum requirements: diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 4d5bb5237..ea67c8610 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -31,6 +31,7 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' +ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 535ba3150..52ef83950 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -47,6 +47,7 @@ dependencyManagement { } imports { mavenBom "org.grails:grails-bom:$grailsVersion" + mavenBom "org.springframework:spring-framework-bom:4.3.1.RELEASE" } applyMavenExclusions false } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 91aea3b80..157e71255 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,6 +27,7 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' +ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 21699808e..cae3b9a8a 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,6 +18,7 @@ UTF-8 1.7 + 4.3.1.RELEASE 1.2.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 1be4c82f1..89a457416 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,6 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } +ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index f1e791c84..b0be86971 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,6 +29,8 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } + +ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index 41e8fec3e..75c48e665 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -63,7 +63,7 @@ private Charset extractCharsetFromContentTypeHeader() { if (contentType == null) { return null; } - return contentType.getCharSet(); + return contentType.getCharset(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 7fe67754c..f33f6be66 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -49,8 +49,8 @@ class PatternReplacingContentModifier implements ContentModifier { @Override public byte[] modifyContent(byte[] content, MediaType contentType) { String original; - if (contentType != null && contentType.getCharSet() != null) { - original = new String(content, contentType.getCharSet()); + if (contentType != null && contentType.getCharset() != null) { + original = new String(content, contentType.getCharset()); } else { original = new String(content); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 1a4798147..609729437 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -203,8 +203,8 @@ private void setPort(String port) { @Override public byte[] modifyContent(byte[] content, MediaType contentType) { String input; - if (contentType != null && contentType.getCharSet() != null) { - input = new String(content, contentType.getCharSet()); + if (contentType != null && contentType.getCharset() != null) { + input = new String(content, contentType.getCharset()); } else { input = new String(content); From bf9b0f2d640aee805f9f3b097573f95753d344c5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jul 2016 21:10:21 +0100 Subject: [PATCH 0332/1059] Ensure that query string is not duplicated when parameters overlap Closes gh-286 --- .../restdocs/cli/CliOperationRequest.java | 28 ------- .../restdocs/cli/CurlRequestSnippet.java | 14 ++-- .../restdocs/cli/HttpieRequestSnippet.java | 24 +++--- .../restdocs/http/HttpRequestSnippet.java | 9 ++- .../restdocs/operation/Parameters.java | 35 ++++++++ .../restdocs/cli/CurlRequestSnippetTests.java | 72 +++++++++++++++-- .../cli/HttpieRequestSnippetTests.java | 72 +++++++++++++++-- .../http/HttpRequestSnippetTests.java | 81 +++++++++++++++++++ 8 files changed, 276 insertions(+), 59 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index ce305278a..b88994104 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -52,34 +52,6 @@ final class CliOperationRequest implements OperationRequest { new BasicAuthHeaderFilter(), new HostHeaderFilter(delegate.getUri()))); } - Parameters getUniqueParameters() { - Parameters queryStringParameters = new QueryStringParser() - .parse(this.delegate.getUri()); - Parameters uniqueParameters = new Parameters(); - - for (Map.Entry> parameter : this.delegate.getParameters() - .entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Map.Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - boolean isPutOrPost() { return HttpMethod.PUT.equals(this.delegate.getMethod()) || HttpMethod.POST.equals(this.delegate.getMethod()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 31127c3c4..4f8d603cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -70,9 +70,12 @@ protected Map createModel(Operation operation) { private String getUrl(Operation operation) { OperationRequest request = operation.getRequest(); - if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { - return String.format("'%s?%s'", request.getUri(), - request.getParameters().toQueryString()); + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(operation.getRequest().getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { + return String.format("'%s%s%s'", request.getUri(), + StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", + uniqueParameters.toQueryString()); } return String.format("'%s'", request.getUri()); } @@ -157,9 +160,10 @@ else if (request.isPutOrPost()) { } } - private void writeContentUsingParameters(CliOperationRequest request, + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = request.getUniqueParameters(); + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { writer.print(String.format(" -d '%s'", queryString)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 4c95c04d1..10b96facb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -90,10 +90,13 @@ private String getOptions(CliOperationRequest request) { return options.toString(); } - private String getUrl(CliOperationRequest request) { - if (!request.getUniqueParameters().isEmpty() && includeParametersInUri(request)) { - return String.format("'%s?%s'", request.getUri(), - request.getParameters().toQueryString()); + private String getUrl(OperationRequest request) { + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { + return String.format("'%s%s%s'", request.getUri(), + StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", + uniqueParameters.toQueryString()); } return String.format("'%s'", request.getUri()); } @@ -107,14 +110,15 @@ private String getRequestItems(CliOperationRequest request) { return requestItems.toString(); } - private void writeOptions(CliOperationRequest request, PrintWriter writer) { - if (!request.getParts().isEmpty() || (!request.getUniqueParameters().isEmpty() - && !includeParametersInUri(request))) { + private void writeOptions(OperationRequest request, PrintWriter writer) { + if (!request.getParts().isEmpty() + || (!request.getParameters().getUniqueParameters(request.getUri()) + .isEmpty() && !includeParametersInUri(request))) { writer.print("--form "); } } - private boolean includeParametersInUri(CliOperationRequest request) { + private boolean includeParametersInUri(OperationRequest request) { return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; } @@ -167,7 +171,9 @@ private void writeParametersIfNecessary(CliOperationRequest request, writeContentUsingParameters(request.getParameters(), writer); } else if (request.isPutOrPost()) { - writeContentUsingParameters(request.getUniqueParameters(), writer); + writeContentUsingParameters( + request.getParameters().getUniqueParameters(request.getUri()), + writer); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index c966f137e..0110dd1b6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -30,6 +30,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -75,12 +76,14 @@ protected Map createModel(Operation operation) { private String getPath(OperationRequest request) { String path = request.getUri().getRawPath(); String queryString = request.getUri().getRawQuery(); - if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { if (StringUtils.hasText(queryString)) { - queryString = queryString + "&" + request.getParameters().toQueryString(); + queryString = queryString + "&" + uniqueParameters.toQueryString(); } else { - queryString = request.getParameters().toQueryString(); + queryString = uniqueParameters.toQueryString(); } } if (StringUtils.hasText(queryString)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index b696c2927..bd05f4216 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -17,10 +17,12 @@ package org.springframework.restdocs.operation; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLEncoder; import java.util.List; import java.util.Map; +import org.springframework.restdocs.cli.QueryStringParser; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StringUtils; @@ -53,6 +55,39 @@ public String toQueryString() { return sb.toString(); } + /** + * Returns a new {@code Parameters} containing only the parameters that do no appear + * in the query string of the given {@code uri}. + * + * @param uri the uri + * @return the unique parameters + */ + public Parameters getUniqueParameters(URI uri) { + Parameters queryStringParameters = new QueryStringParser().parse(uri); + Parameters uniqueParameters = new Parameters(); + + for (Map.Entry> parameter : entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Map.Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } + } + } + } + private static void append(StringBuilder sb, String key) { append(sb, key, ""); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index e26ca2176..314234b85 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -95,6 +95,47 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?param=value") + .param("param", "value").build()); + } + + @Test + public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getRequestWithDisjointQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("b", "bravo") + .build()); + } + @Test public void getRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectCurlRequest("request-with-query-string-with-no-value") @@ -172,25 +213,42 @@ public void postRequestWithUrlEncodedParameter() throws IOException { } @Test - public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string-and-parameter") + public void postRequestWithDisjointQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-disjoint-query-string-and-parameter") .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string-and-parameter") + new CurlRequestSnippet().document( + operationBuilder("post-request-with-disjoint-query-string-and-parameter") .request("http://localhost/foo?a=alpha").method("POST") .param("b", "bravo").build()); } @Test - public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + public void postRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { this.snippet .expectCurlRequest( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet().document(operationBuilder( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .request("http://localhost/foo?a=alpha").method("POST") .param("a", "alpha").param("b", "bravo").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 9f8164b39..9dd297c6f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -96,6 +96,47 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?param=value") + .param("param", "value").build()); + } + + @Test + public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getRequestWithDisjointQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("b", "bravo") + .build()); + } + @Test public void getRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") @@ -173,25 +214,42 @@ public void postRequestWithUrlEncodedParameter() throws IOException { } @Test - public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string-and-parameter") + public void postRequestWithDisjointQueryStringAndParameter() throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-disjoint-query-string-and-parameter") .withContents(codeBlock("bash").content( "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string-and-parameter") + new HttpieRequestSnippet().document( + operationBuilder("post-request-with-disjoint-query-string-and-parameter") .request("http://localhost/foo?a=alpha").method("POST") .param("b", "bravo").build()); } @Test - public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + public void postRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "post-request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { this.snippet .expectHttpieRequest( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .withContents(codeBlock("bash").content( "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .request("http://localhost/foo?a=alpha").method("POST") .param("a", "alpha").param("b", "bravo").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 6f7057b37..e66e6b5be 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -102,6 +102,34 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .request("http://localhost/foo?bar").build()); } + @Test + public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpRequest( + "get-with-partially-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder( + "get-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpRequest( + "get-with-totally-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder( + "get-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo") + .param("a", "alpha").param("b", "bravo").build()); + } + @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; @@ -128,6 +156,59 @@ public void postRequestWithContentAndParameters() throws IOException { .param("a", "alpha").content(content).build()); } + @Test + public void postRequestWithContentAndDisjointQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-disjoint-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-disjoint-query-string-and-parameters") + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").content(content).build()); + } + + @Test + public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").content(content) + .build()); + } + + @Test + public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?b=bravo&a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").content(content) + .build()); + } + @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; From b430a887965f3cb4c2f15060e67ce5b666538333 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Aug 2016 17:08:18 +0100 Subject: [PATCH 0333/1059] Polishing --- .../restdocs/curl/CurlRequestSnippet.java | 5 +---- .../org/springframework/restdocs/hypermedia/Link.java | 7 ++----- .../springframework/restdocs/test/SnippetMatchers.java | 10 +++++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4f4ea07b7..50764ecde 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -223,10 +223,7 @@ private static final class BasicAuthHeaderFilter implements HeaderFilter { @Override public boolean allow(String name, List value) { - if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { - return false; - } - return true; + return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)); } static boolean isBasicAuthHeader(List value) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 112886f0b..5e8a34f3b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -80,10 +80,7 @@ public boolean equals(Object obj) { if (!this.href.equals(other.href)) { return false; } - if (!this.rel.equals(other.rel)) { - return false; - } - return true; + return this.rel.equals(other.rel); } @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 98c1ccd10..7f23ad8ea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -82,10 +82,14 @@ protected void addLine(String line) { } protected void addLine(int index, String line) { - if (index < 0) { - index = index + this.lines.size(); + this.lines.add(determineIndex(index), line); + } + + private int determineIndex(int index) { + if (index >= 0) { + return index; } - this.lines.add(index, line); + return index + this.lines.size(); } @Override From 9ba0a9b6be0b9be039382ee6ef8efb5073df1579 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:30:22 +0100 Subject: [PATCH 0334/1059] Stop field snippets requiring a type for missing, optional, ignored field Missing optional fields require a type to be explicitly provided as it cannot be inferred from the payload. Ignored fields are not included in the documentation, yet, previously, a field that was missing, optional, and ignored would still require a type to be provided otherwise the test would fail. This was pointless as the field wasn't going to appear in the documentation. This commit updates the fields snippets so that a type is no longer required for a field that is missing, optional and ignored. Closes gh-289 --- .../payload/AbstractFieldsSnippet.java | 20 ++++++++++--------- .../payload/RequestFieldsSnippetTests.java | 15 ++++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 14 +++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index a21bf524c..16f7ba61b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -100,15 +100,17 @@ protected Map createModel(Operation operation) { validateFieldDocumentation(contentHandler); for (FieldDescriptor descriptor : this.fieldDescriptors) { - try { - descriptor.type(contentHandler.determineFieldType(descriptor)); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the " - + "payload. Please provide a type using " - + "FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + if (!descriptor.isIgnored()) { + try { + descriptor.type(contentHandler.determineFieldType(descriptor)); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the " + + "payload. Please provide a type using " + + "FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); + } } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 32e34be9e..18e89a9b0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -118,6 +118,21 @@ public void missingOptionalRequestField() throws IOException { .request("http://localhost").content("{}").build()); } + @Test + public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() + throws IOException { + this.snippet + .expectRequestFields( + "missing-ignored-optional-request-field-does-not-require-a-type") + .withContents(tableWithHeader("Path", "Type", "Description")); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder( + "missing-ignored-optional-request-field-does-not-require-a-type") + .request("http://localhost").content("{}") + .build()); + } + @Test public void presentOptionalRequestField() throws IOException { this.snippet.expectRequestFields("present-optional-request-field") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index e0525888d..8d93a9872 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -147,6 +147,20 @@ public void missingOptionalResponseField() throws IOException { .response().content("{}").build()); } + @Test + public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() + throws IOException { + this.snippet + .expectResponseFields( + "missing-ignored-optional-response-field-does-not-require-a-type") + .withContents(tableWithHeader("Path", "Type", "Description")); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder( + "missing-ignored-optional-response-field-does-not-require-a-type") + .response().content("{}").build()); + } + @Test public void presentOptionalResponseField() throws IOException { this.snippet.expectResponseFields("present-optional-response-field") From a0faebdb0a33f2ccede8c1293a778d78ddccd7fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:51:16 +0100 Subject: [PATCH 0335/1059] Ignore query string when extracting path parameters Closes gh-285 --- .../restdocs/request/PathParametersSnippet.java | 2 +- .../request/PathParametersSnippetTests.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index afbd0b527..64df77ec5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -116,7 +116,7 @@ private String removeQueryStringIfPresent(String urlTemplate) { @Override protected Set extractActualParameters(Operation operation) { - String urlTemplate = extractUrlTemplate(operation); + String urlTemplate = removeQueryStringIfPresent(extractUrlTemplate(operation)); Matcher matcher = NAMES_PATTERN.matcher(urlTemplate); Set actualParameters = new HashSet<>(); while (matcher.find()) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index e09809918..fbc7449b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -130,6 +130,23 @@ public void pathParametersWithQueryString() throws IOException { "/{a}/{b}?foo=bar").build()); } + @Test + public void pathParametersWithQueryStringWithParameters() throws IOException { + this.snippet + .expectPathParameters("path-parameters-with-query-string-with-parameters") + .withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))) + .document(operationBuilder( + "path-parameters-with-query-string-with-parameters") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo={c}") + .build()); + } + @Test public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 5c6cf7f12ff43ed97dcc2e59e1b63465d91ee175 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:51:37 +0100 Subject: [PATCH 0336/1059] Polishing --- .../restdocs/request/PathParametersSnippet.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 64df77ec5..254cb7b42 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -129,9 +129,8 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes() .get(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); - Assert.notNull(urlTemplate, - "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " - + "build the request?"); + Assert.notNull(urlTemplate, "urlTemplate not found. If you are using MockMvc did " + + "you use RestDocumentationRequestBuilders to build the request?"); return urlTemplate; } From 14dcdb61d06fddbe40bc31f9bab2d00e8de73b99 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 20:14:16 +0100 Subject: [PATCH 0337/1059] Fix package tangle between cli and operation packages QueryStringParser has been moved into the operation page. In the unlikely event that there were any external users of the class, a deprecated version remains in the cli package for backwards compatibility. It will be removed in 1.2. Closes gh-286 --- .../restdocs/cli/QueryStringParser.java | 73 +-------------- .../restdocs/operation/Parameters.java | 1 - .../restdocs/operation/QueryStringParser.java | 91 +++++++++++++++++++ .../QueryStringParserTests.java | 6 +- 4 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{cli => operation}/QueryStringParserTests.java (94%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java index 4a3ab8ca1..2ebeed543 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java @@ -16,78 +16,15 @@ package org.springframework.restdocs.cli; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.util.LinkedList; -import java.util.List; -import java.util.Scanner; - -import org.springframework.restdocs.operation.Parameters; - /** * A parser for the query string of a URI. * * @author Andy Wilkinson + * @deprecated since 1.1.2 in favor of + * {@link org.springframework.restdocs.operation.QueryStringParser} */ -public class QueryStringParser { - - /** - * Parses the query string of the given {@code uri} and returns the resulting - * {@link Parameters}. - * - * @param uri the uri to parse - * @return the parameters parsed from the query string - */ - public Parameters parse(URI uri) { - String query = uri.getRawQuery(); - if (query != null) { - return parse(query); - } - return new Parameters(); - } - - private Parameters parse(String query) { - Parameters parameters = new Parameters(); - try (Scanner scanner = new Scanner(query)) { - scanner.useDelimiter("&"); - while (scanner.hasNext()) { - processParameter(scanner.next(), parameters); - } - } - return parameters; - } - - private void processParameter(String parameter, Parameters parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.get(components[0]); - if (values == null) { - parameters.put(components[0], new LinkedList()); - } - } - } - else { - throw new IllegalArgumentException( - "The parameter '" + parameter + "' is malformed"); - } - } - - private String decode(String encoded) { - try { - return URLDecoder.decode(encoded, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException( - "Unable to URL encode " + encoded + " using UTF-8", ex); - } - - } +@Deprecated +public class QueryStringParser + extends org.springframework.restdocs.operation.QueryStringParser { } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index bd05f4216..f2ec42a5e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.cli.QueryStringParser; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StringUtils; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java new file mode 100644 index 000000000..c5ab3b703 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2016 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.operation; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.get(components[0]); + if (values == null) { + parameters.put(components[0], new LinkedList()); + } + } + } + else { + throw new IllegalArgumentException( + "The parameter '" + parameter + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException( + "Unable to URL encode " + encoded + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java similarity index 94% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java index 3f30bd89e..21684d564 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli; +package org.springframework.restdocs.operation; import java.net.URI; import java.util.Arrays; @@ -23,8 +23,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.operation.Parameters; - import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasEntry; From 21567e0ffa10a330951727edca62f283ca7f8803 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 22 Aug 2016 15:45:26 +0100 Subject: [PATCH 0338/1059] Simply dealing with operation names in REST Docs' own tests --- .../restdocs/AbstractSnippetTests.java | 9 +- .../restdocs/cli/CurlRequestSnippetTests.java | 272 +++++++----------- .../cli/HttpieRequestSnippetTests.java | 272 +++++++----------- .../RequestHeadersSnippetFailureTests.java | 20 +- .../headers/RequestHeadersSnippetTests.java | 99 ++++--- .../ResponseHeadersSnippetFailureTests.java | 16 +- .../headers/ResponseHeadersSnippetTests.java | 59 ++-- .../http/HttpRequestSnippetTests.java | 202 ++++++------- .../http/HttpResponseSnippetTests.java | 42 ++- .../hypermedia/LinksSnippetFailureTests.java | 21 +- .../hypermedia/LinksSnippetTests.java | 68 ++--- .../AsciidoctorRequestFieldsSnippetTests.java | 32 ++- .../RequestFieldsSnippetFailureTests.java | 87 +++--- .../payload/RequestFieldsSnippetTests.java | 108 +++---- .../ResponseFieldsSnippetFailureTests.java | 84 +++--- .../payload/ResponseFieldsSnippetTests.java | 136 +++++---- .../PathParametersSnippetFailureTests.java | 37 ++- .../request/PathParametersSnippetTests.java | 116 ++++---- .../RequestParametersSnippetFailureTests.java | 26 +- .../RequestParametersSnippetTests.java | 109 ++++--- .../RequestPartsSnippetFailureTests.java | 21 +- .../request/RequestPartsSnippetTests.java | 98 +++---- .../restdocs/test/ExpectedSnippet.java | 51 ++-- .../restdocs/test/OperationBuilder.java | 55 +++- 24 files changed, 917 insertions(+), 1123 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 0adc3d0b5..41b18ffda 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -52,6 +52,9 @@ public abstract class AbstractSnippetTests { @Rule public ExpectedSnippet snippet; + @Rule + public OperationBuilder operationBuilder; + @Parameters(name = "{0}") public static List parameters() { return Arrays.asList(new Object[] { "Asciidoctor", asciidoctor() }, @@ -61,6 +64,7 @@ public static List parameters() { public AbstractSnippetTests(String name, TemplateFormat templateFormat) { this.snippet = new ExpectedSnippet(templateFormat); this.templateFormat = templateFormat; + this.operationBuilder = new OperationBuilder(this.templateFormat); } public CodeBlockMatcher codeBlock(String language) { @@ -84,11 +88,6 @@ public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { return SnippetMatchers.httpResponse(this.templateFormat, responseStatus); } - public OperationBuilder operationBuilder(String name) { - return new OperationBuilder(name, this.snippet.getOutputDirectory(), - this.templateFormat); - } - protected FileSystemResource snippetResource(String name) { return new FileSystemResource("src/test/resources/custom-snippet-templates/" + this.templateFormat.getId() + "/" + name + ".snippet"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 314234b85..c9e2ead72 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -55,245 +55,197 @@ public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectCurlRequest("get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document( - operationBuilder("get-request").request("http://localhost/foo").build()); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo").build()); } @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectCurlRequest("get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); - new CurlRequestSnippet().document(operationBuilder("get-request") + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); } @Test public void nonGetRequest() throws IOException { - this.snippet.expectCurlRequest("non-get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(operationBuilder("non-get-request") + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest("request-with-content") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(operationBuilder("request-with-content") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -d 'content'")); + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("request-with-query-string") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(operationBuilder("request-with-query-string") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); } @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?param=value") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param=value") .param("param", "value").build()); } @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("b", "bravo") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest("request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param' -i")); - new CurlRequestSnippet() - .document(operationBuilder("request-with-query-string-with-no-value") - .request("http://localhost/foo?param").build()); + this.snippet.expectCurlRequest().withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?param' -i")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string") - .request("http://localhost/foo?param=value").method("POST") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param=value").method("POST").build()); } @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param' -i -X POST")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string-with-no-value") - .request("http://localhost/foo?param").method("POST").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param").method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() - .document(operationBuilder("post-request-with-one-parameter") - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); - new CurlRequestSnippet().document( - operationBuilder("post-request-with-one-parameter-with-no-value") - .request("http://localhost/foo").method("POST").param("k1") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("k1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest("post-request-with-multiple-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-multiple-parameters") - .request("http://localhost/foo").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo").method("POST") .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); new CurlRequestSnippet() - .document(operationBuilder("post-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("POST").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-disjoint-query-string-and-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document( - operationBuilder("post-request-with-disjoint-query-string-and-parameter") - .request("http://localhost/foo?a=alpha").method("POST") - .param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); } @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); - new CurlRequestSnippet().document(operationBuilder( - "post-request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(operationBuilder( - "post-request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet() - .document(operationBuilder("put-request-with-one-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest("put-request-with-multiple-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(operationBuilder("put-request-with-multiple-parameters") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); new CurlRequestSnippet() - .document(operationBuilder("put-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest("request-with-headers") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document( - operationBuilder("request-with-headers").request("http://localhost/foo") + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -304,15 +256,12 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; - this.snippet.expectCurlRequest("multipart-post-no-original-filename") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-no-original-filename") - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @Test @@ -320,11 +269,10 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest("multipart-post-with-content-type") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -337,11 +285,10 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest("multipart-post") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -354,11 +301,10 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest("multipart-post-with-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -368,10 +314,10 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest("basic-auth").withContents(codeBlock("bash") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); new CurlRequestSnippet() - .document(operationBuilder("basic-auth").request("http://localhost/foo") + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) @@ -380,7 +326,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes") + this.snippet.expectCurlRequest() .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) @@ -389,7 +335,7 @@ public void customAttributes() throws IOException { attributes( key("title").value("curl request title"))) .document( - operationBuilder("custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -398,12 +344,12 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectCurlRequest("custom-host-header") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document( - operationBuilder("custom-host-header").request("http://localhost/foo") + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) @@ -412,15 +358,13 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectCurlRequest("post-with-content-and-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X POST -d 'Some content'")); - new CurlRequestSnippet() - .document(operationBuilder("post-with-content-and-parameters") - .request("http://localhost/foo").param("a", "alpha") - .method("POST").param("b", "bravo").content("Some content") - .build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").param("a", "alpha").method("POST") + .param("b", "bravo").content("Some content").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 9dd297c6f..527a2c7f8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -56,245 +56,197 @@ public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpieRequest("get-request").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'")); - new HttpieRequestSnippet().document( - operationBuilder("get-request").request("http://localhost/foo").build()); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo").build()); } @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpieRequest("get-request-with-parameter").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); - new HttpieRequestSnippet().document(operationBuilder("get-request-with-parameter") + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); } @Test public void nonGetRequest() throws IOException { - this.snippet.expectHttpieRequest("non-get-request").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo'")); - new HttpieRequestSnippet().document(operationBuilder("non-get-request") + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectHttpieRequest("request-with-content") - .withContents(codeBlock("bash") - .content("$ echo 'content' | http GET 'http://localhost/foo'")); - new HttpieRequestSnippet().document(operationBuilder("request-with-content") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest("request-with-query-string") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(operationBuilder("request-with-query-string") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); } @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?param=value") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param=value") .param("param", "value").build()); } @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("b", "bravo") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param'")); - new HttpieRequestSnippet() - .document(operationBuilder("request-with-query-string-with-no-value") - .request("http://localhost/foo?param").build()); + this.snippet.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo?param'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string") - .request("http://localhost/foo?param=value").method("POST") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param=value").method("POST").build()); } @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?param'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string-with-no-value") - .request("http://localhost/foo?param").method("POST").build()); + this.snippet.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http POST 'http://localhost/foo?param'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param").method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-one-parameter") - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-one-parameter-with-no-value") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1='")); - new HttpieRequestSnippet().document( - operationBuilder("post-request-with-one-parameter-with-no-value") - .request("http://localhost/foo").method("POST").param("k1") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("k1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-multiple-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-multiple-parameters") - .request("http://localhost/foo").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo").method("POST") .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("POST").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-disjoint-query-string-and-parameter") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet().document( - operationBuilder("post-request-with-disjoint-query-string-and-parameter") - .request("http://localhost/foo?a=alpha").method("POST") - .param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); } @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); - new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-one-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-multiple-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-multiple-parameters") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest("request-with-headers").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet().document( - operationBuilder("request-with-headers").request("http://localhost/foo") + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -305,15 +257,12 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); - this.snippet.expectHttpieRequest("multipart-post-no-original-filename") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-no-original-filename") - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @Test @@ -322,11 +271,10 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest("multipart-post-with-content-type") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -339,11 +287,10 @@ public void multipartPost() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest("multipart-post") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -356,11 +303,10 @@ public void multipartPostWithParameters() throws IOException { .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + " 'b=banana'"); - this.snippet.expectHttpieRequest("multipart-post-with-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -370,10 +316,10 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() - .document(operationBuilder("basic-auth").request("http://localhost/foo") + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) @@ -382,7 +328,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectHttpieRequest("custom-attributes") + this.snippet.expectHttpieRequest() .withContents(containsString("httpie request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("httpie-request")) @@ -391,7 +337,7 @@ public void customAttributes() throws IOException { attributes( key("title").value("httpie request title"))) .document( - operationBuilder("custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -400,12 +346,12 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectHttpieRequest("custom-host-header") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content( "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet().document( - operationBuilder("custom-host-header").request("http://localhost/foo") + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) @@ -414,14 +360,12 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectHttpieRequest("post-with-content-and-parameters").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ echo 'Some content' | http POST " + "'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-with-content-and-parameters") - .request("http://localhost/foo").method("POST") - .param("a", "alpha").param("b", "bravo").content("Some content") - .build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("a", "alpha") + .param("b", "bravo").content("Some content").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index 33ccabb27..f1192567c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -24,13 +24,13 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,10 +56,8 @@ public void missingRequestHeader() throws IOException { .expectMessage(equalTo("Headers with the following names were not found" + " in the request: [Accept]")); new RequestHeadersSnippet( - Arrays.asList(headerWithName("Accept").description("one"))) - .document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()) - .request("http://localhost").build()); + Arrays.asList(headerWithName("Accept").description("one"))).document( + this.operationBuilder.request("http://localhost").build()); } @Test @@ -67,11 +68,8 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio + " in the request: [Accept]")); new RequestHeadersSnippet( Arrays.asList(headerWithName("Accept").description("one"))) - .document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test").build()); + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 5766661c5..805c1f7cb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -27,7 +27,6 @@ import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; @@ -50,7 +49,7 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers") + this.snippet.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -62,64 +61,71 @@ public void requestWithHeaders() throws IOException { headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four"), headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six"))) - .document(operationBuilder("request-with-headers") - .request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + headerWithName( + "Connection") + .description("six"))) + .document( + this.operationBuilder + .request( + "http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept-Language", + "en-US,en;q=0.5") + .header("Cache-Control", + "max-age=0") + .header("Connection", + "keep-alive") + .build()); } @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet.expectRequestHeaders("case-insensitive-request-headers") - .withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); + this.snippet.expectRequestHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(operationBuilder("case-insensitive-request-headers") - .request("/").header("X-test", "test").build()); + .document(this.operationBuilder.request("/") + .header("X-test", "test").build()); } @Test public void undocumentedRequestHeader() throws IOException { new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(new OperationBuilder("undocumented-request-header", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .build()); + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .build()); } @Test public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectRequestHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-title")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("request-headers-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test") - .build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .header("X-Test", "test").build()); } @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet - .expectRequestHeaders("request-headers-with-custom-descriptor-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); + this.snippet.expectRequestHeaders().withContents(// + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-extra-column")); @@ -130,8 +136,8 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), headerWithName("Accept").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "request-headers-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -144,7 +150,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestHeaders("additional-descriptors") + this.snippet.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -156,10 +162,12 @@ public void additionalDescriptors() throws IOException { headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four")) .and(headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").header("X-Test", "test") - .header("Accept", "*/*") + headerWithName( + "Connection") + .description( + "six")) + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") .header("Accept-Language", "en-US,en;q=0.5") .header("Cache-Control", "max-age=0") @@ -168,14 +176,13 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents( + this.snippet.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new RequestHeadersSnippet( Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(operationBuilder("request-with-escaped-headers") - .request("http://localhost").header("Foo|Bar", "baz") - .build()); + .document(this.operationBuilder.request("http://localhost") + .header("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index 997a3bc8a..e42d38179 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -24,13 +24,13 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or @@ -41,7 +41,10 @@ public class ResponseHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -54,8 +57,7 @@ public void missingResponseHeader() throws IOException { + " in the response: [Content-Type]")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(new OperationBuilder("missing-response-headers", - this.snippet.getOutputDirectory()).response().build()); + .document(this.operationBuilder.response().build()); } @Test @@ -66,10 +68,8 @@ public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOExcept + " in the response: [Content-Type]")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", - this.snippet.getOutputDirectory()).response() - .header("X-Test", "test").build()); + .document(this.operationBuilder.response() + .header("X-Test", "test").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 417259cba..86ac3eef6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -27,7 +27,6 @@ import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; @@ -50,7 +49,7 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -61,7 +60,7 @@ public void responseWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six"))) .document( - operationBuilder("response-headers").response() + this.operationBuilder.response() .header("X-Test", "test") .header("Content-Type", "application/json") @@ -72,44 +71,44 @@ public void responseWithHeaders() throws IOException { @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet.expectResponseHeaders("case-insensitive-response-headers") - .withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); + this.snippet.expectResponseHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(operationBuilder("case-insensitive-response-headers") - .response().header("X-test", "test").build()); + .document(this.operationBuilder.response() + .header("X-test", "test").build()); } @Test public void undocumentedResponseHeader() throws IOException { new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))) - .document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response() - .header("X-Test", "test") - .header("Content-Type", "*/*").build()); + Arrays.asList(headerWithName("X-Test").description("one"))).document( + this.operationBuilder.response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); } @Test public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectResponseHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")) .willReturn(snippetResource("response-headers-with-title")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("response-headers-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response().header("X-Test", "test").build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().header("X-Test", "test") + .build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + this.snippet.expectResponseHeaders() .withContents(tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); @@ -123,8 +122,8 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), headerWithName("Etag").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "response-headers-with-custom-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -137,7 +136,7 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders("additional-descriptors").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -146,10 +145,8 @@ public void additionalDescriptors() throws IOException { headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three")) .and(headerWithName("Cache-Control").description("five"), - headerWithName("Vary") - .description("six")) - .document(operationBuilder("additional-descriptors").response() - .header("X-Test", "test") + headerWithName("Vary").description("six")) + .document(this.operationBuilder.response().header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0").header("Vary", "User-Agent") @@ -158,13 +155,13 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(operationBuilder("response-with-escaped-headers") - .response().header("Foo|Bar", "baz").build()); + .document(this.operationBuilder.response() + .header("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index e66e6b5be..6ba4d65a3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -51,245 +51,222 @@ public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest("get-request") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder("get-request") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").header("Alpha", "a").build()); } @Test public void getRequestWithParameters() throws IOException { - this.snippet.expectHttpRequest("get-request-with-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder("get-request-with-parameters") - .request("http://localhost/foo").header("Alpha", "a").param("b", "bravo") - .build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .header("Alpha", "a").param("b", "bravo").build()); } @Test public void getRequestWithPort() throws IOException { - this.snippet.expectHttpRequest("get-request") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost:8080")); - new HttpRequestSnippet().document(operationBuilder("get-request") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost:8080/foo").header("Alpha", "a").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet() - .document(operationBuilder("get-request-with-query-string") - .request("http://localhost/foo?bar=baz").build()); + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?bar=baz").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string-with-no-value") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet() - .document(operationBuilder("get-request-with-query-string-with-no-value") - .request("http://localhost/foo?bar").build()); + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?bar").build()); } @Test public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpRequest( - "get-with-partially-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder( - "get-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpRequest( - "get-with-totally-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder( - "get-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") .param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder("post-request-with-content") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").content(content).build()); } @Test public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() - .document(operationBuilder("post-request-with-content-and-parameters") - .request("http://localhost/foo").method("POST") - .param("a", "alpha").content(content).build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("a", "alpha").content(content).build()); } @Test public void postRequestWithContentAndDisjointQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-disjoint-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-disjoint-query-string-and-parameters") - .request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").content(content).build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?b=bravo") + .method("POST").param("a", "alpha").content(content).build()); } @Test public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").content(content) - .build()); + new HttpRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").content(content).build()); } @Test public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?b=bravo&a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").content(content) - .build()); + new HttpRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?b=bravo&a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest("post-request-with-charset") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(operationBuilder("post-request-with-charset") - .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") - .request("http://localhost/foo").method("POST").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("b&r", "baz").param("a", "alpha").build()); } @Test public void postRequestWithParameterWithNoValue() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("bar=")); - new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("bar").build()); } @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("put-request-with-content") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder("put-request-with-content") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").content(content).build()); } @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("put-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(operationBuilder("put-request-with-parameter") - .request("http://localhost/foo").method("PUT").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("b&r", "baz").param("a", "alpha").build()); } @Test public void multipartPost() throws IOException { String expectedContent = createPart(String.format( "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - this.snippet - .expectHttpRequest( - "multipart-post") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()).build()); @@ -309,16 +286,13 @@ public void multipartPostWithParameters() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet - .expectHttpRequest( - "multipart-post-with-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a", "apple", "avocado").param("b", "banana") @@ -332,16 +306,13 @@ public void multipartPostWithParameterWithNoValue() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = paramPart + filePart; - this.snippet - .expectHttpRequest( - "multipart-post-with-parameter-with-no-value") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-parameter-with-no-value") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a").part("image", "<< data >>".getBytes()).build()); @@ -352,16 +323,13 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet - .expectHttpRequest( - "multipart-post-with-content-type") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -371,26 +339,30 @@ public void multipartPostWithContentType() throws IOException { @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest("get-request-custom-host") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo") .header(HttpHeaders.HOST, "api.example.com")); - new HttpRequestSnippet().document(operationBuilder("get-request-custom-host") - .request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com").build()); } @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest("request-with-snippet-attributes") + this.snippet.expectHttpRequest() .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) .willReturn(snippetResource("http-request-with-title")); - new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) - .document(operationBuilder("request-with-snippet-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + new HttpRequestSnippet( + attributes( + key("title").value("Title for the request"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost/foo").build()); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 120812eea..8560fd399 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -49,26 +49,24 @@ public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response") - .withContents(httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(operationBuilder("basic-response").build()); + this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); + new HttpResponseSnippet().document(this.operationBuilder.build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse("non-ok-response") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(operationBuilder("non-ok-response").response() + new HttpResponseSnippet().document(this.operationBuilder.response() .status(HttpStatus.BAD_REQUEST.value()).build()); } @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse("response-with-headers") - .withContents(httpResponse(HttpStatus.OK) - .header("Content-Type", "application/json").header("a", "alpha")); + this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet() - .document(operationBuilder("response-with-headers").response() + .document(this.operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -77,41 +75,37 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse("response-with-content") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK).content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpResponseSnippet().document(operationBuilder("response-with-content") - .response().content(content).build()); + new HttpResponseSnippet() + .document(this.operationBuilder.response().content(content).build()); } @Test public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse("response-with-charset") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); - new HttpResponseSnippet().document(operationBuilder("response-with-charset") - .response().header("Content-Type", "text/plain;charset=UTF-8") - .content(contentBytes).build()); + new HttpResponseSnippet().document(this.operationBuilder.response() + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse("response-with-snippet-attributes") + this.snippet.expectHttpResponse() .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) .willReturn(snippetResource("http-response-with-title")); - new HttpResponseSnippet( - attributes(key("title").value("Title for the response"))) - .document( - operationBuilder("response-with-snippet-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); + new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) + .document(this.operationBuilder.attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index b8f994703..f8e0cf762 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -25,11 +25,11 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented @@ -40,7 +40,10 @@ public class LinksSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -52,8 +55,7 @@ public void undocumentedLink() throws IOException { "Links with the following relations were not" + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), Collections.emptyList()) - .document(new OperationBuilder("undocumented-link", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -63,8 +65,7 @@ public void missingLink() throws IOException { + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -74,9 +75,8 @@ public void undocumentedLinkAndMissingLink() throws IOException { + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( - new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(this.operationBuilder.build()); } @Test @@ -87,8 +87,7 @@ public void linkWithNoDescription() throws IOException { + " title was available from the link in the payload")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), Arrays.asList(new LinkDescriptor("foo"))) - .document(new OperationBuilder("link-with-no-description", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index eb14248b7..3236bd8ec 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,70 +47,66 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { - this.snippet.expectLinks("ignored-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), new LinkDescriptor("b").description("Link b"))) - .document(operationBuilder("ignored-link").build()); + .document(this.operationBuilder.build()); } @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { - this.snippet.expectLinks("ignore-all-undocumented").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("b").description("Link b")), true) - .document(operationBuilder("ignore-all-undocumented").build()); + .document(this.operationBuilder.build()); } @Test public void presentOptionalLink() throws IOException { - this.snippet.expectLinks("present-optional-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(operationBuilder("present-optional-link").build()); + .document(this.operationBuilder.build()); } @Test public void missingOptionalLink() throws IOException { - this.snippet.expectLinks("missing-optional-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(operationBuilder("missing-optional-link").build()); + .document(this.operationBuilder.build()); } @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks("documented-links") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "two")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "two")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))) - .document(operationBuilder("documented-links").build()); + .document(this.operationBuilder.build()); } @Test public void linkDescriptionFromTitleInPayload() throws IOException { - this.snippet.expectLinks("link-description-from-title-in-payload") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "Link b")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), Arrays.asList(new LinkDescriptor("a").description("one"), - new LinkDescriptor("b"))).document( - operationBuilder("link-description-from-title-in-payload") - .build()); + new LinkDescriptor("b"))).document(this.operationBuilder.build()); } @Test @@ -118,8 +114,7 @@ public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-title")); - this.snippet.expectLinks("links-with-custom-attributes") - .withContents(containsString("Title for the links")); + this.snippet.expectLinks().withContents(containsString("Title for the links")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -128,7 +123,7 @@ public void linksWithCustomAttributes() throws IOException { new LinkDescriptor("b").description("two")), attributes(key("title").value("Title for the links"))) .document( - operationBuilder("links-with-custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .build()); @@ -139,7 +134,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-extra-column")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes") + this.snippet.expectLinks() .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -149,38 +144,33 @@ public void linksWithCustomDescriptorAttributes() throws IOException { Arrays.asList( new LinkDescriptor("a").description("one") .attributes(key("foo").value("alpha")), - new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo")))) - .document(operationBuilder( - "links-with-custom-descriptor-attributes") - .attribute(TemplateEngine.class - .getName(), - new MustacheTemplateEngine( - resolver)) - .build()); + new LinkDescriptor("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(this.operationBuilder.attribute( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectLinks("additional-descriptors") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "two")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), new LinkDescriptor("a").description("one")) .and(new LinkDescriptor("b").description("two")) - .document(operationBuilder("additional-descriptors").build()); + .document(this.operationBuilder.build()); } @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectLinks("links-with-escaped-content") - .withContents(tableWithHeader("Relation", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) - .document(operationBuilder("links-with-escaped-content").build()); + .document(this.operationBuilder.build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 69c32563d..d1a83a662 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -42,6 +42,9 @@ */ public class AsciidoctorRequestFieldsSnippetTests { + @Rule + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + @Rule public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @@ -50,21 +53,22 @@ public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader(asciidoctor(), "Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); + this.snippet.expectRequestFields().withContents( + tableWithHeader(asciidoctor(), "Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); - new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("a").description(Arrays.asList("one", "two")))).document( - new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost") - .content("{\"a\": \"foo\"}").build()); + new RequestFieldsSnippet( + Arrays.asList( + fieldWithPath("a").description(Arrays.asList("one", "two")))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index f27633c2f..30d16443a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -47,6 +47,10 @@ public class RequestFieldsSnippetFailureTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + @Rule + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -56,9 +60,8 @@ public void undocumentedRequestField() throws IOException { this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5}").build()); } @Test @@ -67,9 +70,8 @@ public void missingRequestField() throws IOException { this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{}").build()); + .document(this.operationBuilder.request("http://localhost").content("{}") + .build()); } @Test @@ -77,11 +79,8 @@ public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").optional())) - .document(new OperationBuilder( - "missing-optional-request-field-with-no-type", - this.snippet.getOutputDirectory()) - .request("http://localhost").content("{ }") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ }").build()); } @Test @@ -93,10 +92,8 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a.b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder( - "undocumented-request-field-and-missing-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ \"a\": { \"c\": 5 }}").build()); } @Test @@ -105,9 +102,7 @@ public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { this.thrown.expectMessage( equalTo("Cannot document request fields as the request body is empty")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("no-request-body", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + .document(this.operationBuilder.request("http://localhost").build()); } @Test @@ -117,10 +112,8 @@ public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException + " Object but the actual type is Number")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("{ \"a\": 5 }").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ \"a\": 5 }").build()); } @Test @@ -130,11 +123,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException + " Object but the actual type is Varies")); new RequestFieldsSnippet(Arrays.asList( fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); } @Test @@ -143,23 +133,20 @@ public void undocumentedXmlRequestField() throws IOException { this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -168,15 +155,10 @@ public void missingXmlRequestField() throws IOException { this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))) - .document( - new OperationBuilder("missing-xml-request-fields", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))).document(this.operationBuilder + .request("http://localhost").content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -189,13 +171,10 @@ public void undocumentedXmlRequestFieldAndMissingXmlRequestField() .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 18e89a9b0..b88f6a51f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -50,7 +50,7 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -58,15 +58,14 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))) - .document(operationBuilder("map-request-with-fields") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); } @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") @@ -75,8 +74,7 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))) - .document(operationBuilder("array-request-with-fields") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); @@ -84,64 +82,57 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields("ignored-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(operationBuilder("ignored-request-field") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { - this.snippet.expectRequestFields("ignore-all-undocumented") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true).document( - operationBuilder("ignore-all-undocumented") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void missingOptionalRequestField() throws IOException { - this.snippet.expectRequestFields("missing-optional-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("missing-optional-request-field") - .request("http://localhost").content("{}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{}").build()); } @Test public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() throws IOException { - this.snippet - .expectRequestFields( - "missing-ignored-optional-request-field-does-not-require-a-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(operationBuilder( - "missing-ignored-optional-request-field-does-not-require-a-type") - .request("http://localhost").content("{}") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("{}").build()); } @Test public void presentOptionalRequestField() throws IOException { - this.snippet.expectRequestFields("present-optional-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("present-optional-request-field") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": { \"b\": \"bravo\"}}").build()); } @@ -150,16 +141,18 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectRequestFields().withContents(containsString("Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("request-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}") - .build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test @@ -167,8 +160,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-extra-column")); - this.snippet - .expectRequestFields("request-fields-with-custom-descriptor-attributes") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -181,8 +173,8 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), fieldWithPath("a").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "request-fields-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -194,35 +186,31 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet - .expectRequestFields("request-field-with-explicit-exactly-matching-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(operationBuilder( - "request-field-with-explicit-exactly-matching-type") - .request("http://localhost") - .content("{\"a\": 5 }").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5 }").build()); } @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectRequestFields("request-field-with-explicit-varies-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type(JsonFieldType.VARIES))).document( - operationBuilder("request-field-with-explicit-varies-type") - .request("http://localhost").content("{\"a\": 5 }") - .build()); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5 }").build()); } @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields("xml-request") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -232,8 +220,7 @@ public void xmlRequestFields() throws IOException { fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) .document( - operationBuilder("xml-request") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -242,7 +229,7 @@ public void xmlRequestFields() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestFields("additional-descriptors") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -251,14 +238,13 @@ public void additionalDescriptors() throws IOException { .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost") + .document(operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectRequestFields("prefixed-additional-descriptors") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -266,23 +252,21 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder("prefixed-additional-descriptors") - .request("http://localhost") + .document(operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void requestWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectRequestFields("request-fields-with-escaped-content") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); new RequestFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder("request-fields-with-escaped-content") - .request("http://localhost").content("{\"Foo|Bar\": 5}") - .build()); + .document(operationBuilder.request("http://localhost") + .content("{\"Foo|Bar\": 5}").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index bfe50ad0c..05b1b9c17 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -27,7 +27,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -35,6 +34,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.startsWith; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or @@ -45,7 +45,10 @@ public class ResponseFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -56,8 +59,7 @@ public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { this.thrown.expectMessage( equalTo("Cannot document response fields as the response body is empty")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("no-response-body", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -67,9 +69,8 @@ public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException + " Object but the actual type is Number")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()).response() - .content("{ \"a\": 5 }}").build()); + .document(this.operationBuilder.response() + .content("{ \"a\": 5 }}").build()); } @Test @@ -79,10 +80,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException + " Object but the actual type is Varies")); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()).response() - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") - .build()); + .document(this.operationBuilder.response() + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); } @Test @@ -90,15 +89,10 @@ public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); - new ResponseFieldsSnippet( - Collections.emptyList()) - .document( - new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()).response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet(Collections.emptyList()) + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -109,8 +103,8 @@ public void missingXmlAttribute() throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", - this.snippet.getOutputDirectory()).response() + .document( + this.operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -125,24 +119,20 @@ public void documentedXmlAttributesAreRemoved() throws IOException { + "%nbar%n"))); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) - .document( - new OperationBuilder("documented-attribute-is-removed", - this.snippet.getOutputDirectory()).response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", - this.snippet.getOutputDirectory()).response().content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -151,14 +141,10 @@ public void missingXmlResponseField() throws IOException { this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a/b]")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))) - .document( - new OperationBuilder("missing-xml-response-field", - this.snippet.getOutputDirectory()).response() - .content("") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))).document(this.operationBuilder + .response().content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -170,16 +156,10 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); - new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one"))) - .document( - new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()).response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8d93a9872..daaaa233e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -50,7 +50,7 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields("map-response-with-fields") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -63,7 +63,7 @@ public void mapResponseWithFields() throws IOException { fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(operationBuilder("map-response-with-fields").response() + .document(this.operationBuilder.response() .content( "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") @@ -72,15 +72,15 @@ public void mapResponseWithFields() throws IOException { @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") .row("`[]a`", "`Object`", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three"))).document( - operationBuilder("array-response-with-fields").response() + fieldWithPath("[]a").description("three"))) + .document(this.operationBuilder.response() .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); @@ -88,35 +88,35 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { - this.snippet.expectResponseFields("array-response") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(operationBuilder("array-response").response() + .document(this.operationBuilder.response() .content("[\"a\", \"b\", \"c\"]").build()); } @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields("ignored-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(operationBuilder("ignored-response-field").response() + .document(this.operationBuilder.response() .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { - this.snippet.expectResponseFields("ignore-all-undocumented") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(operationBuilder("ignore-all-undocumented").response() + .document(this.operationBuilder.response() .content("{\"a\": 5, \"b\": 4}").build()); } @@ -125,50 +125,48 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectResponseFields().withContents(containsString("Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("response-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response().content("{\"a\": \"foo\"}").build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().content("{\"a\": \"foo\"}") + .build()); } @Test public void missingOptionalResponseField() throws IOException { - this.snippet.expectResponseFields("missing-optional-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("missing-optional-response-field") - .response().content("{}").build()); + .document(this.operationBuilder.response().content("{}").build()); } @Test public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() throws IOException { - this.snippet - .expectResponseFields( - "missing-ignored-optional-response-field-does-not-require-a-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(operationBuilder( - "missing-ignored-optional-response-field-does-not-require-a-type") - .response().content("{}").build()); + .document(this.operationBuilder.response().content("{}").build()); } @Test public void presentOptionalResponseField() throws IOException { - this.snippet.expectResponseFields("present-optional-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())).document( - operationBuilder("present-optional-response-field").response() + .type(JsonFieldType.STRING).optional())) + .document(this.operationBuilder.response() .content("{\"a\": { \"b\": \"bravo\"}}").build()); } @@ -177,58 +175,57 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-extra-column")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") .row("a", "Object", "three", "charlie")); new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one").attributes(key("foo") - .value("alpha")), + fieldWithPath("a.b").description("one") + .attributes(key("foo").value("alpha")), fieldWithPath("a.c").description("two") .attributes(key("foo").value("bravo")), fieldWithPath("a").description("three") - .attributes(key("foo").value("charlie")))).document( - operationBuilder("response-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response() - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + .attributes(key("foo").value("charlie")))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet - .expectResponseFields( - "response-field-with-explicit-exactly-matching-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(operationBuilder( - "response-field-with-explicit-exactly-matching-type") - .response().content("{\"a\": 5 }").build()); + .document(this.operationBuilder.response().content("{\"a\": 5 }") + .build()); } @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectResponseFields("response-field-with-explicit-varies-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type(JsonFieldType.VARIES))).document( - operationBuilder("response-field-with-explicit-varies-type") - .response().content("{\"a\": 5 }").build()); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(this.operationBuilder.response().content("{\"a\": 5 }") + .build()); } @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields("xml-response") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -237,7 +234,7 @@ public void xmlResponseFields() throws IOException { fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) .document( - operationBuilder("xml-response").response() + this.operationBuilder.response() .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -246,14 +243,14 @@ public void xmlResponseFields() throws IOException { @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields("xml-attribute") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) .document( - operationBuilder("xml-attribute").response() + this.operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -262,15 +259,15 @@ public void xmlAttribute() throws IOException { @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields("missing-optional-xml-attribute") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c").optional())) .document( - operationBuilder("missing-optional-xml-attribute") - .response().content("foo") + this.operationBuilder.response() + .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -278,12 +275,11 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { - this.snippet.expectResponseFields("undocumented-attribute").withContents( + this.snippet.expectResponseFields().withContents( tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("a"))) - .document(operationBuilder("undocumented-attribute").response() - .content("bar") + Arrays.asList(fieldWithPath("a").description("one").type("a"))).document( + this.operationBuilder.response().content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -291,7 +287,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseFields("additional-descriptors") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -305,7 +301,7 @@ public void additionalDescriptors() throws IOException { .and(fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six")) - .document(operationBuilder("additional-descriptors").response() + .document(this.operationBuilder.response() .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") .build()); @@ -313,7 +309,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectResponseFields("prefixed-additional-descriptors") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -321,21 +317,21 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder("prefixed-additional-descriptors").response() + .document(this.operationBuilder.response() .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void responseWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectResponseFields("response-fields-with-escaped-content") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder("response-fields-with-escaped-content") - .response().content("{\"Foo|Bar\": 5}").build()); + .document(this.operationBuilder.response() + .content("{\"Foo|Bar\": 5}").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 99ab6a0ba..26f0f6b09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -26,12 +26,12 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link PathParametersSnippet} due to missing or @@ -42,7 +42,10 @@ public class PathParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,12 +56,10 @@ public void undocumentedPathParameter() throws IOException { this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not documented: [a]")); new PathParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/") - .build()); + .document(this.operationBuilder + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/") + .build()); } @Test @@ -68,12 +69,9 @@ public void missingPathParameter() throws IOException { + " not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/") - .build()); + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/").build()); } @Test @@ -83,13 +81,10 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " not documented: [b]. Path parameters with the following" + " names were not found in the request: [a]")); new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - new OperationBuilder("undocumented-and-missing-path-parameters", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{b}") - .build()); + Arrays.asList(parameterWithName("a").description("one"))) + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{b}").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index fbc7449b5..c74f5d401 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -49,102 +49,94 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { - this.snippet.expectPathParameters("path-parameters").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(operationBuilder("path-parameters").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}").build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); } @Test public void ignoredPathParameter() throws IOException { - this.snippet.expectPathParameters("ignored-path-parameter").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(operationBuilder("ignored-path-parameter").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}").build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); } @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { - this.snippet.expectPathParameters("ignore-all-undocumented").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented").attribute( + .document(this.operationBuilder.attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}").build()); } @Test public void missingOptionalPathParameter() throws IOException { - this.snippet - .expectPathParameters( - "missing-optional-path-parameter") - .withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" - : "`/{a}`", - "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", + "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two").optional())).document( - operationBuilder("missing-optional-path-parameter").attribute( + parameterWithName("b").description("two").optional())) + .document(this.operationBuilder.attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); } @Test public void presentOptionalPathParameter() throws IOException { - this.snippet - .expectPathParameters( - "present-optional-path-parameter") - .withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" - : "`/{a}`", - "Parameter", "Description").row("`a`", "one")); + this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", + "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-path-parameter") - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}") - .build()); + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}").build()); } @Test public void pathParametersWithQueryString() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-query-string") - .withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))).document( - operationBuilder("path-parameters-with-query-string").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo=bar").build()); + parameterWithName("b").description("two"))) + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo=bar") + .build()); } @Test public void pathParametersWithQueryStringWithParameters() throws IOException { - this.snippet - .expectPathParameters("path-parameters-with-query-string-with-parameters") - .withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(operationBuilder( - "path-parameters-with-query-string-with-parameters") - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo={c}") - .build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo={c}") + .build()); } @Test @@ -152,8 +144,7 @@ public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectPathParameters().withContents(containsString("The title")); new PathParametersSnippet( Arrays.asList( @@ -161,8 +152,8 @@ public void pathParametersWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))).document( - operationBuilder("path-parameters-with-custom-attributes") + attributes(key("title").value("The title"))) + .document(this.operationBuilder .attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") @@ -177,17 +168,16 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-extra-column")); - this.snippet - .expectPathParameters("path-parameters-with-custom-descriptor-attributes") + this.snippet.expectPathParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(operationBuilder( - "path-parameters-with-custom-descriptor-attributes") + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(this.operationBuilder .attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") @@ -198,12 +188,12 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectPathParameters("additional-descriptors").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") + .document(this.operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") .build()); @@ -211,14 +201,14 @@ public void additionalDescriptors() throws IOException { @Test public void pathParametersWithEscapedContent() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-escaped-content") + this.snippet.expectPathParameters() .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); RequestDocumentation .pathParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("path-parameters-with-escaped-content") + .document(this.operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{Foo|Bar}") .build()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index 79cea42b5..ce50cc892 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -25,12 +25,12 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,9 +56,8 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", - this.snippet.getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "alpha").build()); } @Test @@ -65,10 +67,8 @@ public void missingParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-parameter", - this.snippet.getOutputDirectory()) - .request("http://localhost").build()); + Arrays.asList(parameterWithName("a").description("one"))).document( + this.operationBuilder.request("http://localhost").build()); } @Test @@ -79,11 +79,9 @@ public void undocumentedAndMissingParameters() throws IOException { + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - new OperationBuilder("undocumented-and-missing-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo") - .build()); + Arrays.asList(parameterWithName("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .param("b", "bravo").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 78dac57dd..cffbb671a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,72 +48,66 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters("request-parameters") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))) - .document(operationBuilder("request-parameters") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + parameterWithName("b").description("two"))).document( + this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void requestParameterWithNoValue() throws IOException { - this.snippet.expectRequestParameters("request-parameter-with-no-value") - .withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); + this.snippet.expectRequestParameters().withContents( + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(operationBuilder("request-parameter-with-no-value") - .request("http://localhost").param("a").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a").build()); } @Test public void ignoredRequestParameter() throws IOException { - this.snippet.expectRequestParameters("ignored-request-parameter").withContents( + this.snippet.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(operationBuilder("ignored-request-parameter") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( + this.snippet.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void missingOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters("missing-optional-request-parameter") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))).document( - operationBuilder("missing-optional-request-parameter") - .request("http://localhost").param("b", "bravo") - .build()); + this.operationBuilder.request("http://localhost") + .param("b", "bravo").build()); } @Test public void presentOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters("present-optional-request-parameter") - .withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); + this.snippet.expectRequestParameters().withContents( + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-request-parameter") - .request("http://localhost").param("a", "one").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "one").build()); } @Test @@ -121,8 +115,7 @@ public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectRequestParameters().withContents(containsString("The title")); new RequestParametersSnippet( Arrays.asList( @@ -130,12 +123,16 @@ public void requestParametersWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))).document( - operationBuilder("request-parameters-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").param("a", "alpha") - .param("b", "bravo").build()); + attributes( + key("title").value("The title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test @@ -143,9 +140,7 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-extra-column")); - this.snippet - .expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -154,8 +149,8 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo")))) - .document(operationBuilder( - "request-parameters-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -169,42 +164,44 @@ public void requestParametersWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-optional-column")); - this.snippet.expectRequestParameters("request-parameters-with-optional-column") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").optional(), - parameterWithName("b").description("two"))).document( - operationBuilder("request-parameters-with-optional-column") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").param("a", "alpha") - .param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParameters("additional-descriptors") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void requestParametersWithEscapedContent() throws IOException { - this.snippet.expectRequestParameters("request-parameters-with-escaped-content") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); RequestDocumentation .requestParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("request-parameters-with-escaped-content") - .request("http://localhost").param("Foo|Bar", "baz").build()); + .document(this.operationBuilder.request("http://localhost") + .param("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index b349f0f03..522598201 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -25,12 +25,12 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestPartsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -52,9 +55,8 @@ public void undocumentedPart() throws IOException { this.thrown.expectMessage(equalTo( "Request parts with the following names were" + " not documented: [a]")); new RequestPartsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-part", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("a", "alpha".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "alpha".getBytes()).build()); } @Test @@ -63,9 +65,7 @@ public void missingPart() throws IOException { this.thrown.expectMessage(equalTo("Request parts with the following names were" + " not found in the request: [a]")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(new OperationBuilder("missing-part", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + .document(this.operationBuilder.request("http://localhost").build()); } @Test @@ -75,9 +75,8 @@ public void undocumentedAndMissingParts() throws IOException { + " not documented: [b]. Request parts with the following" + " names were not found in the request: [a]")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(new OperationBuilder("undocumented-and-missing-parts", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("b", "bravo".getBytes()).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 6468d80b6..ecdce573a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -48,59 +48,57 @@ public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestParts() throws IOException { - this.snippet.expectRequestParts("request-parts") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), partWithName("b").description("two"))) - .document(operationBuilder("request-parts") - .request("http://localhost").part("a", "bravo".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void ignoredRequestPart() throws IOException { - this.snippet.expectRequestParts("ignored-request-part") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) - .document(operationBuilder("ignored-request-part") - .request("http://localhost").part("a", "bravo".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { - this.snippet.expectRequestParts("ignore-all-undocumented") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented") - .request("http://localhost").part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) + .build()); } @Test public void missingOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts("missing-optional-request-parts") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))).document( - operationBuilder("missing-optional-request-parts") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .part("b", "bravo".getBytes()).build()); } @Test public void presentOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts("present-optional-request-part") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-request-part") - .request("http://localhost").part("a", "one".getBytes()) - .build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "one".getBytes()).build()); } @Test @@ -108,8 +106,7 @@ public void requestPartsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-title")); - this.snippet.expectRequestParts("request-parts-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectRequestParts().withContents(containsString("The title")); new RequestPartsSnippet( Arrays.asList( @@ -117,12 +114,16 @@ public void requestPartsWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), partWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(operationBuilder("request-parts-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").part("a", "alpha".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + attributes( + key("title").value("The title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test @@ -130,7 +131,7 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-extra-column")); - this.snippet.expectRequestParts("request-parts-with-custom-descriptor-attributes") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -139,8 +140,8 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("alpha")), partWithName("b").description("two") .attributes(key("foo").value("bravo")))) - .document(operationBuilder( - "request-parts-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -154,43 +155,44 @@ public void requestPartsWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-optional-column")); - this.snippet.expectRequestParts("request-parts-with-optional-column") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), - partWithName("b").description("two"))).document( - operationBuilder("request-parts-with-optional-column") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost") - .part("a", "alpha".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + partWithName("b").description("two"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParts("additional-descriptors") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); RequestDocumentation.requestParts(partWithName("a").description("one")) .and(partWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) + .build()); } @Test public void requestPartsWithEscapedContent() throws IOException { - this.snippet.expectRequestParts("request-parts-with-escaped-content") - .withContents(tableWithHeader("Part", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + this.snippet.expectRequestParts().withContents( + tableWithHeader("Part", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("request-parts-with-escaped-content") - .request("http://localhost").part("Foo|Bar", "baz".getBytes()) - .build()); + .document(this.operationBuilder.request("http://localhost") + .part("Foo|Bar", "baz".getBytes()).build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index bc1c77856..e5975f86d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -71,68 +71,67 @@ private void verifySnippet() throws IOException { } } - public ExpectedSnippet expectCurlRequest(String name) { - expect(name, "curl-request"); + public ExpectedSnippet expectCurlRequest() { + expect("curl-request"); return this; } - public ExpectedSnippet expectHttpieRequest(String name) { - expect(name, "httpie-request"); + public ExpectedSnippet expectHttpieRequest() { + expect("httpie-request"); return this; } - public ExpectedSnippet expectRequestFields(String name) { - expect(name, "request-fields"); + public ExpectedSnippet expectRequestFields() { + expect("request-fields"); return this; } - public ExpectedSnippet expectResponseFields(String name) { - expect(name, "response-fields"); + public ExpectedSnippet expectResponseFields() { + expect("response-fields"); return this; } - public ExpectedSnippet expectRequestHeaders(String name) { - expect(name, "request-headers"); + public ExpectedSnippet expectRequestHeaders() { + expect("request-headers"); return this; } - public ExpectedSnippet expectResponseHeaders(String name) { - expect(name, "response-headers"); + public ExpectedSnippet expectResponseHeaders() { + expect("response-headers"); return this; } - public ExpectedSnippet expectLinks(String name) { - expect(name, "links"); + public ExpectedSnippet expectLinks() { + expect("links"); return this; } - public ExpectedSnippet expectHttpRequest(String name) { - expect(name, "http-request"); + public ExpectedSnippet expectHttpRequest() { + expect("http-request"); return this; } - public ExpectedSnippet expectHttpResponse(String name) { - expect(name, "http-response"); + public ExpectedSnippet expectHttpResponse() { + expect("http-response"); return this; } - public ExpectedSnippet expectRequestParameters(String name) { - expect(name, "request-parameters"); + public ExpectedSnippet expectRequestParameters() { + expect("request-parameters"); return this; } - public ExpectedSnippet expectPathParameters(String name) { - expect(name, "path-parameters"); + public ExpectedSnippet expectPathParameters() { + expect("path-parameters"); return this; } - public ExpectedSnippet expectRequestParts(String name) { - expect(name, "request-parts"); + public ExpectedSnippet expectRequestParts() { + expect("request-parts"); return this; } - private ExpectedSnippet expect(String name, String type) { - this.expectedName = name; + private ExpectedSnippet expect(String type) { this.expectedType = type; return this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 3032a6f5d..8acc87b0f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -24,6 +24,10 @@ import java.util.List; import java.util.Map; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -45,7 +49,6 @@ import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -54,28 +57,21 @@ * * @author Andy Wilkinson */ -public class OperationBuilder { +public class OperationBuilder implements TestRule { private final Map attributes = new HashMap<>(); - private final OperationResponseBuilder responseBuilder = new OperationResponseBuilder(); + private OperationResponseBuilder responseBuilder; - private final String name; + private String name; - private final File outputDirectory; + private File outputDirectory; private final TemplateFormat templateFormat; private OperationRequestBuilder requestBuilder; - public OperationBuilder(String name, File outputDirectory) { - this(name, outputDirectory, TemplateFormats.asciidoctor()); - } - - public OperationBuilder(String name, File outputDirectory, - TemplateFormat templateFormat) { - this.name = name; - this.outputDirectory = outputDirectory; + public OperationBuilder(TemplateFormat templateFormat) { this.templateFormat = templateFormat; } @@ -85,6 +81,7 @@ public OperationRequestBuilder request(String uri) { } public OperationResponseBuilder response() { + this.responseBuilder = new OperationResponseBuilder(); return this.responseBuilder; } @@ -93,6 +90,14 @@ public OperationBuilder attribute(String name, Object value) { return this; } + private void prepare(String operationName, File outputDirectory) { + this.name = operationName; + this.outputDirectory = outputDirectory; + this.requestBuilder = null; + this.requestBuilder = null; + this.attributes.clear(); + } + public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { Map templateContext = new HashMap<>(); @@ -113,7 +118,10 @@ public Operation build() { (this.requestBuilder == null ? new OperationRequestBuilder("http://localhost/").buildRequest() : this.requestBuilder.buildRequest()), - this.responseBuilder.buildResponse(), this.attributes); + this.responseBuilder == null + ? new OperationResponseBuilder().buildResponse() + : this.responseBuilder.buildResponse(), + this.attributes); } private RestDocumentationContext createContext() { @@ -124,6 +132,25 @@ private RestDocumentationContext createContext() { return context; } + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + String operationName = description.getMethodName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + OperationBuilder.this.prepare(operationName, + new File("build/" + description.getTestClass().getSimpleName())); + base.evaluate(); + } + + }; + } + /** * Basic builder API for creating an {@link OperationRequest}. */ From 4e4d01a459d3c2972fb1f4a7220978b19ffec165 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Aug 2016 12:49:03 +0100 Subject: [PATCH 0339/1059] Consolidate logic in test rules for getting output dir and operation name --- .../headers/RequestHeadersSnippetTests.java | 2 + .../headers/ResponseHeadersSnippetTests.java | 2 + .../restdocs/test/ExpectedSnippet.java | 11 ++-- .../restdocs/test/OperationBuilder.java | 23 ++------ .../restdocs/test/OperationTestRule.java | 54 +++++++++++++++++++ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 805c1f7cb..373e83b70 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -93,6 +93,8 @@ public void caseInsensitiveRequestHeaders() throws IOException { @Test public void undocumentedRequestHeader() throws IOException { + this.snippet.expectRequestHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(this.operationBuilder.request("http://localhost") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 86ac3eef6..ba572598d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -81,6 +81,8 @@ public void caseInsensitiveResponseHeaders() throws IOException { @Test public void undocumentedResponseHeader() throws IOException { + this.snippet.expectResponseHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))).document( this.operationBuilder.response().header("X-Test", "test") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index e5975f86d..8036e98e0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -20,8 +20,6 @@ import java.io.IOException; import org.hamcrest.Matcher; -import org.junit.rules.TestRule; -import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -38,7 +36,7 @@ * @author Andy Wilkinson * @author Andreas Evers */ -public class ExpectedSnippet implements TestRule { +public class ExpectedSnippet extends OperationTestRule { private final TemplateFormat templateFormat; @@ -56,9 +54,10 @@ public ExpectedSnippet(TemplateFormat templateFormat) { } @Override - public Statement apply(final Statement base, Description description) { - this.outputDirectory = new File( - "build/" + description.getTestClass().getSimpleName()); + public Statement apply(final Statement base, File outputDirectory, + String operationName) { + this.outputDirectory = outputDirectory; + this.expectedName = operationName; return new ExpectedSnippetStatement(base); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 8acc87b0f..698a80dac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -24,8 +24,6 @@ import java.util.List; import java.util.Map; -import org.junit.rules.TestRule; -import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -57,7 +55,7 @@ * * @author Andy Wilkinson */ -public class OperationBuilder implements TestRule { +public class OperationBuilder extends OperationTestRule { private final Map attributes = new HashMap<>(); @@ -133,22 +131,9 @@ private RestDocumentationContext createContext() { } @Override - public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - String operationName = description.getMethodName(); - int index = operationName.indexOf('['); - if (index > 0) { - operationName = operationName.substring(0, index); - } - OperationBuilder.this.prepare(operationName, - new File("build/" + description.getTestClass().getSimpleName())); - base.evaluate(); - } - - }; + public Statement apply(Statement base, File outputDirectory, String operationName) { + prepare(operationName, outputDirectory); + return base; } /** diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java new file mode 100644 index 000000000..28ea176d5 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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.test; + +import java.io.File; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Abstract base class for Operation-related {@link TestRule TestRules}. + * + * @author Andy Wilkinson + */ +abstract class OperationTestRule implements TestRule { + + @Override + public final Statement apply(Statement base, Description description) { + return apply(base, determineOutputDirectory(description), + determineOperationName(description)); + } + + private File determineOutputDirectory(Description description) { + return new File("build/" + description.getTestClass().getSimpleName()); + } + + private String determineOperationName(Description description) { + String operationName = description.getMethodName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + + protected abstract Statement apply(Statement base, File outputDirectory, + String operationName); + +} From 0f1f84e6a604532a37e1f760c24629009f8ea424 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Aug 2016 12:29:14 +0100 Subject: [PATCH 0340/1059] Fix JSON field type resolution for top-level and nested array fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the logic that determined the field’s type would incorrectly look at the contents of the array. For example, if the array contained one or more objects, the field’s type would be resolved as Object rather than Array. This commit updates JsonFieldPath to record when a path explicitly identifies an array so that the array itself is used to determine the field’s type rather than its contents. Closes gh-292 --- .../restdocs/payload/JsonFieldPath.java | 19 ++++++-- .../restdocs/payload/JsonFieldProcessor.java | 2 +- .../payload/JsonFieldTypeResolver.java | 2 +- .../restdocs/payload/JsonFieldPathTests.java | 48 ++++++++++++------- .../payload/JsonFieldProcessorTests.java | 12 +++++ .../payload/JsonFieldTypeResolverTests.java | 17 +++++++ .../payload/RequestFieldsSnippetTests.java | 21 ++++---- .../payload/ResponseFieldsSnippetTests.java | 6 +-- 8 files changed, 92 insertions(+), 35 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index 10478a839..03ddfefb3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,16 +43,24 @@ final class JsonFieldPath { private final boolean precise; - private JsonFieldPath(String rawPath, List segments, boolean precise) { + private final boolean array; + + private JsonFieldPath(String rawPath, List segments, boolean precise, + boolean array) { this.rawPath = rawPath; this.segments = segments; this.precise = precise; + this.array = array; } boolean isPrecise() { return this.precise; } + boolean isArray() { + return this.array; + } + List getSegments() { return this.segments; } @@ -63,7 +72,8 @@ public String toString() { static JsonFieldPath compile(String path) { List segments = extractSegments(path); - return new JsonFieldPath(path, segments, matchesSingleValue(segments)); + return new JsonFieldPath(path, segments, matchesSingleValue(segments), + isArraySegment(segments.get(segments.size() - 1))); } static boolean isArraySegment(String segment) { @@ -71,8 +81,9 @@ static boolean isArraySegment(String segment) { } static boolean matchesSingleValue(List segments) { - for (String segment : segments) { - if (isArraySegment(segment)) { + Iterator iterator = segments.iterator(); + while (iterator.hasNext()) { + if (isArraySegment(iterator.next()) && iterator.hasNext()) { return false; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index fb63a2148..dbaf6d88c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -57,7 +57,7 @@ public void foundMatch(Match match) { if (matches.isEmpty()) { throw new FieldDoesNotExistException(path); } - if (path.isPrecise()) { + if ((!path.isArray()) && path.isPrecise()) { return matches.get(0); } else { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java index f5bc6ec3e..25c373cd5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java @@ -44,7 +44,7 @@ else if (fieldType != commonType) { } return commonType; } - return determineFieldType(this.fieldProcessor.extract(fieldPath, payload)); + return determineFieldType(field); } private JsonFieldType determineFieldType(Object fieldValue) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index 043977968..dc71f15ef 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -32,43 +32,59 @@ public class JsonFieldPathTests { @Test - public void singleFieldIsPrecise() { - assertTrue(JsonFieldPath.compile("a").isPrecise()); + public void singleFieldIsPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a"); + assertTrue(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void singleNestedFieldIsPrecise() { - assertTrue(JsonFieldPath.compile("a.b").isPrecise()); + public void singleNestedFieldIsPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a.b"); + assertTrue(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void topLevelArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("[]").isPrecise()); + public void topLevelArrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void fieldBeneathTopLevelArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("[]a").isPrecise()); + public void fieldBeneathTopLevelArrayIsNotPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("[]a"); + assertFalse(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void arrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[]").isPrecise()); + public void arrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void nestedArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a.b[]").isPrecise()); + public void nestedArrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a.b[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void arrayOfArraysIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[][]").isPrecise()); + public void arrayOfArraysIsNotPreciseAndIsAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[][]"); + assertFalse(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void fieldBeneathAnArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[].b").isPrecise()); + public void fieldBeneathAnArrayIsNotPreciseAndIsNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[].b"); + assertFalse(path.isPrecise()); + assertFalse(path.isArray()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 01af20974..395ca4dfb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -55,6 +56,17 @@ public void extractNestedMapEntry() { equalTo((Object) "bravo")); } + @Test + public void extractTopLevelArray() { + List> payload = new ArrayList<>(); + Map bravo = new HashMap<>(); + bravo.put("b", "bravo"); + payload.add(bravo); + payload.add(bravo); + assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("[]"), payload), + equalTo((Object) payload)); + } + @Test public void extractArray() { Map payload = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java index 502bfd29c..e1ba3ec52 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,6 +46,22 @@ public void arrayField() throws IOException { assertFieldType(JsonFieldType.ARRAY, "[]"); } + @Test + public void topLevelArray() throws IOException { + assertThat( + this.fieldTypeResolver.resolveFieldType("[]", + new ObjectMapper().readValue("[{\"a\":\"alpha\"}]", List.class)), + equalTo(JsonFieldType.ARRAY)); + } + + @Test + public void nestedArray() throws IOException { + assertThat( + this.fieldTypeResolver.resolveFieldType("a[]", + createPayload("{\"a\": [{\"b\":\"bravo\"}]}")), + equalTo(JsonFieldType.ARRAY)); + } + @Test public void booleanField() throws IOException { assertFieldType(JsonFieldType.BOOLEAN, "true"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index b88f6a51f..8970a43da 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -67,13 +67,14 @@ public void mapRequestWithFields() throws IOException { public void arrayRequestWithFields() throws IOException { this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") - .row("`[]a.b`", "`Number`", "one") - .row("`[]a.c`", "`String`", "two") - .row("`[]a`", "`Object`", "three")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), - fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three"))) + .row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two") + .row("`[]a.c`", "`String`", "three") + .row("`[]a`", "`Object`", "four")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"), + fieldWithPath("[]a.b").description("two"), + fieldWithPath("[]a.c").description("three"), + fieldWithPath("[]a").description("four"))) .document(this.operationBuilder.request("http://localhost") .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") @@ -238,7 +239,7 @@ public void additionalDescriptors() throws IOException { .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -252,7 +253,7 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -265,7 +266,7 @@ public void requestWithFieldsWithEscapedContent() throws IOException { new RequestFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"Foo|Bar\": 5}").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index daaaa233e..d22fdb332 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -54,7 +54,7 @@ public void mapResponseWithFields() throws IOException { .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Object`", "four") + .row("`assets[]`", "`Array`", "four") .row("`assets[].id`", "`Number`", "five") .row("`assets[].name`", "`String`", "six")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), @@ -90,7 +90,7 @@ public void arrayResponseWithFields() throws IOException { public void arrayResponse() throws IOException { this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", - "`String`", "one")); + "`Array`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) .document(this.operationBuilder.response() .content("[\"a\", \"b\", \"c\"]").build()); @@ -291,7 +291,7 @@ public void additionalDescriptors() throws IOException { .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Object`", "four") + .row("`assets[]`", "`Array`", "four") .row("`assets[].id`", "`Number`", "five") .row("`assets[].name`", "`String`", "six")); PayloadDocumentation From c0e950b3ee7682b7936e213ccf4e68ea13cbb551 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 31 Aug 2016 08:47:58 +0000 Subject: [PATCH 0341/1059] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index af18773ba..cfc375de4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.2.BUILD-SNAPSHOT +version=1.1.3.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 3ac4a1acada45cacc0a7d394edf719315f26b054 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Sun, 9 Oct 2016 12:54:15 +0200 Subject: [PATCH 0342/1059] Provide a default output directory for snippets based on build tool Rather than requiring an output directory to be explcitly configured, a default is now automatically configured based on the build tool that's being used. When using Gradle, snippets will be generated in build/generated-snippets. When using Maven, snippets will be generated in target/generated-snippets. See gh-297 --- config/checkstyle/checkstyle-suppressions.xml | 1 + .../mockmvc/CustomDefaultSnippets.java | 3 +- .../com/example/mockmvc/CustomEncoding.java | 2 +- .../com/example/mockmvc/CustomFormat.java | 2 +- .../mockmvc/CustomUriConfiguration.java | 2 +- .../mockmvc/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 3 +- .../mockmvc/ExampleApplicationTests.java | 3 +- .../example/mockmvc/ParameterizedOutput.java | 2 +- .../restassured/CustomDefaultSnippets.java | 3 +- .../example/restassured/CustomEncoding.java | 2 +- .../com/example/restassured/CustomFormat.java | 2 +- .../restassured/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 3 +- .../restassured/ExampleApplicationTests.java | 3 +- .../restassured/ParameterizedOutput.java | 3 +- .../SampleRestAssuredApplicationTests.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 2 +- .../testng/SampleTestNgApplicationTests.java | 2 +- settings.gradle | 3 +- .../build.gradle | 6 ++ .../extensions/RestDocsExtensionRegistry.java | 45 +++++++++++++ .../extensions/RestDocsSnippetBlockMacro.java | 66 +++++++++++++++++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../RestDocsSnippetBlockMacroTest.java | 54 +++++++++++++++ .../src/test/resources/rest_docs_macro.adoc | 1 + .../restdocs/JUnitRestDocumentation.java | 8 +++ .../restdocs/ManualRestDocumentation.java | 23 +++++++ .../restdocs/RestDocumentation.java | 8 +++ .../RestDocumentationConfigurerTests.java | 3 +- ...tationContextPlaceholderResolverTests.java | 3 +- .../snippet/StandardWriterResolverTests.java | 3 +- .../restdocs/test/OperationBuilder.java | 3 +- ...ckMvcRestDocumentationConfigurerTests.java | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 3 +- ...suredRestDocumentationConfigurerTests.java | 3 +- ...uredRestDocumentationIntegrationTests.java | 3 +- 41 files changed, 245 insertions(+), 47 deletions(-) create mode 100644 spring-restdocs-asciidoctor-extensions/build.gradle create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index f74d92343..2c9a1795b 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -5,4 +5,5 @@ + diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 6c974927c..1644912e9 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -31,8 +31,7 @@ public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 0351216aa..86bbe0daf 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -29,7 +29,7 @@ public class CustomEncoding { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index b0cae3b18..1e85a49aa 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -30,7 +30,7 @@ public class CustomFormat { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index bc3e0e75f..d71549a89 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -29,7 +29,7 @@ public class CustomUriConfiguration { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 31d883549..934a353e7 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -38,8 +38,7 @@ public class EveryTestPreprocessing { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java index fac8c5ca8..5bec235e4 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -30,8 +30,7 @@ public class ExampleApplicationTestNgTests { - public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "target/generated-snippets"); + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 21afd53d3..61a361182 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -30,8 +30,7 @@ public class ExampleApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index d39102bff..3011f4163 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -29,7 +29,7 @@ public class ParameterizedOutput { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 27512e649..39544db0f 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -29,8 +29,7 @@ public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 6e5c6e867..caa2d6231 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -28,7 +28,7 @@ public class CustomEncoding { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index c0d8adb7e..8f6cf4ab1 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -29,7 +29,7 @@ public class CustomFormat { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 43ef84e79..c31cf228a 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -38,8 +38,7 @@ public class EveryTestPreprocessing { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index aa0a57966..aa91cd4e1 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -29,8 +29,7 @@ public class ExampleApplicationTestNgTests { - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "build/generated-snippets"); + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index c7b8d774a..7133906cb 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -28,8 +28,7 @@ public class ExampleApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 88db51d8f..e5f2ce475 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -29,8 +29,7 @@ public class ParameterizedOutput { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java index 92ef63746..6eea9876d 100644 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -42,7 +42,7 @@ public class SampleRestAssuredApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private RequestSpecification documentationSpec; diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index d4ea4ca06..61564aca9 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private NoteRepository noteRepository; 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 2db8fcb40..82a30e34b 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 @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private NoteRepository noteRepository; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 29418eb1c..407a8119c 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -58,7 +58,7 @@ public class GettingStartedDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private ObjectMapper objectMapper; 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 b3cbb58f5..f63458406 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 @@ -70,7 +70,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private RestDocumentationResultHandler documentationHandler; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 9531e5831..e147deeb4 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -61,7 +61,7 @@ public class GettingStartedDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private ObjectMapper objectMapper; diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java index b49883b25..9c4a9c9b6 100644 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -39,7 +39,7 @@ @WebAppConfiguration public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("build/generated-snippets"); + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/settings.gradle b/settings.gradle index 8d6619a19..a31b3171b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'spring-restdocs' include 'docs' +include 'spring-restdocs-asciidoctor-extensions' include 'spring-restdocs-core' include 'spring-restdocs-mockmvc' -include 'spring-restdocs-restassured' \ No newline at end of file +include 'spring-restdocs-restassured' diff --git a/spring-restdocs-asciidoctor-extensions/build.gradle b/spring-restdocs-asciidoctor-extensions/build.gradle new file mode 100644 index 000000000..4c24e8b30 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/build.gradle @@ -0,0 +1,6 @@ +description = 'Spring REST Docs Asciidoctor Extensions' + +dependencies { + compile 'org.asciidoctor:asciidoctorj:1.5.2' + optional 'junit:junit' +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java new file mode 100644 index 000000000..82a8e60bb --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.extension.spi.ExtensionRegistry; + +/** + * ExtensionRegistry for the Spring Rest Docs macros to get registered + * in all projects that include this asciidoctor extension module. + *

            + * Macros provided: + *

              + *
            • {@link RestDocsSnippetBlockMacro}
            • + *
            + * + * @author Gerrit Meier + */ +public class RestDocsExtensionRegistry implements ExtensionRegistry { + + /** + * the name that identifies the block macro in the asciidoctor document + * (e.g.
            snippet::file_to_include[]
            ) + */ + private static final String SNIPPET_BLOCK_NAME = "snippet"; + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry().blockMacro(SNIPPET_BLOCK_NAME, RestDocsSnippetBlockMacro.class); + } +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java new file mode 100644 index 000000000..896133f32 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.OptionsBuilder; +import org.asciidoctor.ast.AbstractBlock; +import org.asciidoctor.extension.BlockMacroProcessor; + +/** + * Block macro to include snippets generated by Spring Rest Docs in a convenient way. + * Defaults to build (gradle) or target directory to look for the generated-snippets folder and its content. + * + * @author Gerrit Meier + */ +class RestDocsSnippetBlockMacro extends BlockMacroProcessor { + + private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; + private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String MAVEN_POM = "pom.xml"; + + public RestDocsSnippetBlockMacro(String macroName, Map config) { + super(macroName, config); + } + + @Override + protected Object process(AbstractBlock parent, String fileToInclude, Map attributes) { + String generatedSnippetPath = getDefaultOutputDirectory() + File.separator + fileToInclude; + + // since 'pass' context does not convert the content, we have to do this manually + String convertedContent = Asciidoctor.Factory.create().convertFile( + new File(generatedSnippetPath), + OptionsBuilder.options().toFile(false).inPlace(false).get()); + + return createBlock(parent, "pass", convertedContent, attributes, getConfig()); + } + + private static String getDefaultOutputDirectory() { + String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); + + if (Files.exists(Paths.get(MAVEN_POM))) { + return executingDirectory + File.separator + MAVEN_TARGET_PATH; + } + return executingDirectory + File.separator + GRADLE_BUILD_PATH; + } +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..084eaa641 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.extensions.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java new file mode 100644 index 000000000..9876af57f --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RestDocsSnippetBlockMacro}. + * + * @author Gerrit Meier + */ +public class RestDocsSnippetBlockMacroTest { + + @Before + public void prepareIncludeFiles() throws Exception { + Files.createDirectories(Paths.get("build/generated-snippets/")); + Files.copy(Paths.get("src/test/resources/rest_docs_macro.adoc"), + Paths.get("build/generated-snippets/rest_docs_macro.adoc"), StandardCopyOption.REPLACE_EXISTING); + } + + @Test + public void replaceRestDocsSnippetBlockWithFile() { + Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + asciidoctor.javaExtensionRegistry().blockMacro("snippet", RestDocsSnippetBlockMacro.class); + + assertThat(asciidoctor.convert("snippet::rest_docs_macro.adoc[]", new Options()), + equalTo("
            \n

            test text

            \n
            ")); + } + +} diff --git a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc new file mode 100644 index 000000000..276e7895a --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc @@ -0,0 +1 @@ +test text \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java index 635b57f54..8825137e8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java @@ -32,6 +32,14 @@ public class JUnitRestDocumentation private final ManualRestDocumentation delegate; + /** + * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets + * to <gradle/maven build path>/generated-snippet. + */ + public JUnitRestDocumentation() { + this.delegate = new ManualRestDocumentation(); + } + /** * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets * to the given {@code outputDirectory}. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index 890d9a4cd..19a8188c5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -17,6 +17,8 @@ package org.springframework.restdocs; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; /** * {@code ManualRestDocumentation} is used to manually manage the @@ -31,10 +33,23 @@ */ public final class ManualRestDocumentation implements RestDocumentationContextProvider { + private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; + private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String MAVEN_POM = "pom.xml"; + private final File outputDirectory; private RestDocumentationContext context; + /** + * Creates a new {@code ManualRestDocumentation} instance that will generate snippets + * to <gradle/maven build path>/generated-snippet. + */ + public ManualRestDocumentation() { + this(getDefaultOutputDirectory()); + } + /** * Creates a new {@code ManualRestDocumentation} instance that will generate snippets * to the given {@code outputDirectory}. @@ -79,4 +94,12 @@ public RestDocumentationContext beforeOperation() { return this.context; } + private static String getDefaultOutputDirectory() { + String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); + + if (Files.exists(Paths.get(MAVEN_POM))) { + return executingDirectory + File.separator + MAVEN_TARGET_PATH; + } + return executingDirectory + File.separator + GRADLE_BUILD_PATH; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java index 51c96aca2..6018ed692 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -32,6 +32,14 @@ public class RestDocumentation implements TestRule, RestDocumentationContextProv private final JUnitRestDocumentation delegate; + /** + * Creates a new {@code RestDocumentation} instance that will generate snippets to the + * to <gradle/maven build path>/generated-snippet. + */ + public RestDocumentation() { + this.delegate = new JUnitRestDocumentation(); + } + /** * Creates a new {@code RestDocumentation} instance that will generate snippets to the * given {@code outputDirectory}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 995a6ffb6..0692cf61c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -195,8 +195,7 @@ public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidocto } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 83f96e611..7fa1d1c4d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -83,8 +83,7 @@ private PlaceholderResolver createResolver(String methodName) { } private RestDocumentationContext createContext(String methodName) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(getClass(), methodName); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index a4d3460d6..49bdd4ccc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -70,8 +70,7 @@ public void configuredOutputAndAbsoluteInput() { } private RestDocumentationContext createContext(String outputDir) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - outputDir); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(outputDir); manualRestDocumentation.beforeTest(getClass(), null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 698a80dac..154c59e23 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -123,8 +123,7 @@ public Operation build() { } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - this.outputDirectory.getAbsolutePath()); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(this.outputDirectory.getAbsolutePath()); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index 281c76169..0377ec0bc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -44,7 +44,7 @@ public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("test"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Test public void defaultConfiguration() { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 2b2df2e8e..5429591fc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -101,8 +101,7 @@ public class MockMvcRestDocumentationIntegrationTests { @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 3417fbbaf..00d889306 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -47,8 +47,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private final FilterableRequestSpecification requestSpec = mock( FilterableRequestSpecification.class); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index f81c91f39..d1089ecc7 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -91,8 +91,7 @@ public class RestAssuredRestDocumentationIntegrationTests { @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Value("${local.server.port}") private int port; From 319bd139fc620757e122306f0b518ef301ca702d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 11:19:06 +0100 Subject: [PATCH 0343/1059] Polish contribution and rework to use default attribute rather than macro Rather than introducing a custom macro, this commit opts to implicitly configure the snippets attribute instead. The attribute is configured will the path into which snippets are generated, relative to the directory that contains the Asciidoctor document that is being rendered. The samples and documentation have been updated to use the new spring-restdocs-asciidoctor module and the implicitly configured snippets attribute. Closes gh-297 --- build.gradle | 1 + config/checkstyle/checkstyle-suppressions.xml | 1 - docs/src/docs/asciidoc/getting-started.adoc | 115 +++++++++--------- .../asciidoc/working-with-asciidoctor.adoc | 7 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/.gitignore | 5 +- samples/rest-notes-grails/build.gradle | 13 +- .../src/docs/{ => asciidoc}/index.adoc | 0 .../com/example/ApiDocumentationSpec.groovy | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 10 +- .../rest-notes-spring-hateoas/build.gradle | 3 +- samples/testng/build.gradle | 4 +- settings.gradle | 2 +- .../build.gradle | 6 - .../extensions/RestDocsSnippetBlockMacro.java | 66 ---------- ...sciidoctor.extension.spi.ExtensionRegistry | 1 - .../RestDocsSnippetBlockMacroTest.java | 54 -------- .../src/test/resources/rest_docs_macro.adoc | 1 - spring-restdocs-asciidoctor/build.gradle | 7 ++ .../DefaultAttributesPreprocessor.java | 43 +++++++ .../RestDocsExtensionRegistry.java | 24 ++-- .../SnippetsDirectoryResolver.java | 58 +++++++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../DefaultAttributesPreprocessorTests.java | 58 +++++++++ .../SnippetsDirectoryResolverTests.java | 68 +++++++++++ .../src/test/resources/sample-snippet.adoc | 4 + .../restdocs/ManualRestDocumentation.java | 24 ++-- .../RestDocumentationConfigurerTests.java | 3 +- ...tationContextPlaceholderResolverTests.java | 3 +- .../snippet/StandardWriterResolverTests.java | 3 +- .../restdocs/test/OperationBuilder.java | 3 +- 31 files changed, 348 insertions(+), 244 deletions(-) rename samples/rest-notes-grails/src/docs/{ => asciidoc}/index.adoc (100%) delete mode 100644 spring-restdocs-asciidoctor-extensions/build.gradle delete mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java delete mode 100644 spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry delete mode 100644 spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java delete mode 100644 spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc create mode 100644 spring-restdocs-asciidoctor/build.gradle create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java rename {spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions => spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor}/RestDocsExtensionRegistry.java (53%) create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java create mode 100644 spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java create mode 100644 spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc diff --git a/build.gradle b/build.gradle index 330e83554..fc664c7e3 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ subprojects { dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' dependency 'junit:junit:4.12' + dependency 'org.asciidoctor:asciidoctorj:1.5.3.1' dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-library:1.3' dependency 'org.hibernate:hibernate-validator:5.2.2.Final' diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 2c9a1795b..f74d92343 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -5,5 +5,4 @@ - diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9b0040ab3..a282bf46f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -94,13 +94,9 @@ the configuration are described below. test - <2> - ${project.build.directory}/generated-snippets - - - <3> + <2> org.apache.maven.plugins maven-surefire-plugin @@ -109,26 +105,30 @@ the configuration are described below. - <4> + <3> org.asciidoctor asciidoctor-maven-plugin - 1.5.2 + 1.5.3 generate-docs - prepare-package <6> + prepare-package <4> process-asciidoc html book - - ${snippetsDirectory} <5> - + + <5> + org.springframework.restdocs + spring-restdocs-asciidoctor + {project-version} + + @@ -136,49 +136,50 @@ the configuration are described below. <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` instead. -<2> Configure a property to define the output location for generated snippets. -<3> Add the SureFire plugin and configure it to include files whose names end with +<2> Add the SureFire plugin and configure it to include files whose names end with `Documentation.java`. -<4> Add the Asciidoctor plugin -<5> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<6> Using `prepare-package` allows the documentation to be +<3> Add the Asciidoctor plugin. +<4> Using `prepare-package` allows the documentation to be <>. +<5> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. This + will automatically configure the `snippets` attribute for use in your `.adoc` files to + point to `target/generated-snippets`. [source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.convert" version "1.5.3" } - dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' + dependencies { + asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' <2> + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' <3> } - ext { <3> + ext { <4> snippetsDir = file('build/generated-snippets') } - test { <4> + test { <5> outputs.dir snippetsDir } - asciidoctor { <5> - attributes 'snippets': snippetsDir <6> + asciidoctor { <6> inputs.dir snippetsDir <7> dependsOn test <8> } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If +<2> Add a dependency on `spring-restdocs-asciidoctor` in the `asciidoctor` configuration. + This will automatically configure the `snippets` attribute for use in your `.adoc` + files to point to `build/generated-snippets`. +<3> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If you want to use REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` instead. -<3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task to add the snippets directory as an output. -<5> Configure the `asciidoctor` task -<6> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. +<4> Configure a property to define the output location for generated snippets. +<5> Configure the `test` task to add the snippets directory as an output. +<6> Configure the `asciidoctor` task <7> Configure the snippets directory as an input. <8> Make the task depend on the test task so that the tests are run before the documentation is created. @@ -271,28 +272,38 @@ are also supported although slightly more setup is required. ===== Setting up your JUnit tests When using JUnit, the first step in generating documentation snippets is to declare a -`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. The -`JUnitRestDocumentation` rule is configured with the output directory into which generated -snippets should be written. This output directory should match the snippets directory that -you have configured in your `build.gradle` or `pom.xml` file. +`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. -For Maven (`pom.xml`) that will typically be `target/generated-snippets` and for -Gradle (`build.gradle`) it will typically be `build/generated-snippets`: -[source,java,indent=0,role="primary"] -.Maven +[source,java,indent=0] ---- @Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("target/generated-snippets"); +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); ---- -[source,java,indent=0,role="secondary"] -.Gradle + +By default, the `JUnitRestDocumentation` rule is automatically configured with an output +directory based on your project's build tool: + +[cols="2,5"] +|=== +| Build tool | Output directory + +| Maven +| `target/generated-snippets` + +| Gradle +| `build/generated-snippets` + +|=== + +The default can be overridden by providing an output directory when creating the +`JUnitRestDocumentation` instance: + +[source,java,indent=0] ---- @Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("build/generated-snippets"); +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom"); ---- Next, provide an `@Before` method to configure MockMvc or REST Assured: @@ -331,18 +342,9 @@ illustrates the approach. The first difference is that `ManualRestDocumentation` should be used in place of `JUnitRestDocumentation` and there's no need for the `@Rule` annotation: -[source,java,indent=0,role="primary"] -.Maven ----- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("target/generated-snippets"); ----- - -[source,java,indent=0,role="secondary"] -.Gradle +[source,java,indent=0] ---- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("build/generated-snippets"); +private ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); ---- Secondly, `ManualRestDocumentation.beforeTest(Class, String)` @@ -441,7 +443,8 @@ the resulting HTML files depends on whether you are using Maven or Gradle: The generated snippets can then be included in the manually created Asciidoctor file from above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. -The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: [source,adoc,indent=0] diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index b3981dfeb..283afcdf7 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -17,10 +17,11 @@ relevant to Spring REST Docs. [[working-with-asciidoctor-including-snippets]] === Including snippets -The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include +The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro] is used to include generated snippets in your documentation. The `snippets` -attribute specified in the <> -can be used to reference the snippets output directory, for example: +attribute that is automatically set by `spring-restdocs-asciidoctor` configured in the +<> can be used to reference the +snippets output directory. For example: [source,adoc,indent=0] ---- diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index ea67c8610..b8776d376 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -37,6 +37,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" } test { @@ -44,7 +45,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore index a521ca89f..18afeb782 100644 --- a/samples/rest-notes-grails/.gitignore +++ b/samples/rest-notes-grails/.gitignore @@ -11,7 +11,4 @@ classes/ .settings .classpath gradlew* -gradle/wrapper - - -src/docs/generated-snippets +gradle/wrapper \ No newline at end of file diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 52ef83950..64e2e6d8e 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -44,6 +44,7 @@ repositories { dependencyManagement { dependencies { dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" + dependency "org.springframework.restdocs:spring-restdocs-asciidoctor:$restDocsVersion" } imports { mavenBom "org.grails:grails-bom:$grailsVersion" @@ -53,6 +54,8 @@ dependencyManagement { } dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor" + compile "org.springframework.boot:spring-boot-starter-logging" compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-autoconfigure" @@ -85,24 +88,16 @@ task wrapper(type: Wrapper) { } ext { - snippetsDir = file('src/docs/generated-snippets') -} - -task cleanTempDirs(type: Delete) { - delete fileTree(dir: 'src/docs/generated-snippets') + snippetsDir = file('build/generated-snippets') } test { - dependsOn cleanTempDirs outputs.dir snippetsDir } asciidoctor { dependsOn integrationTest inputs.dir snippetsDir - sourceDir = file('src/docs') - separateOutputDirs = false - attributes 'snippets': snippetsDir } build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc similarity index 100% rename from samples/rest-notes-grails/src/docs/index.adoc rename to samples/rest-notes-grails/src/docs/asciidoc/index.adoc diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 3f0da9cae..2c014fc01 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -45,7 +45,7 @@ import spock.lang.Specification class ApiDocumentationSpec extends Specification { @Rule - JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Value('${local.server.port}') Integer serverPort diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index cae3b9a8a..022c19226 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -84,12 +84,16 @@ html book - - ${project.build.directory}/generated-snippets - + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + maven-resources-plugin diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 89a457416..a44f3f942 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -34,6 +34,8 @@ ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" + compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-hateoas' @@ -50,7 +52,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index b0be86971..3be74243e 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -34,7 +34,10 @@ ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile('org.springframework:spring-test') { exclude group: 'junit', module: 'junit;' } @@ -48,7 +51,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/settings.gradle b/settings.gradle index a31b3171b..94b5f9b14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,7 @@ rootProject.name = 'spring-restdocs' include 'docs' -include 'spring-restdocs-asciidoctor-extensions' +include 'spring-restdocs-asciidoctor' include 'spring-restdocs-core' include 'spring-restdocs-mockmvc' include 'spring-restdocs-restassured' diff --git a/spring-restdocs-asciidoctor-extensions/build.gradle b/spring-restdocs-asciidoctor-extensions/build.gradle deleted file mode 100644 index 4c24e8b30..000000000 --- a/spring-restdocs-asciidoctor-extensions/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -description = 'Spring REST Docs Asciidoctor Extensions' - -dependencies { - compile 'org.asciidoctor:asciidoctorj:1.5.2' - optional 'junit:junit' -} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java deleted file mode 100644 index 896133f32..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2014-2016 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.asciidoctor.extensions; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.OptionsBuilder; -import org.asciidoctor.ast.AbstractBlock; -import org.asciidoctor.extension.BlockMacroProcessor; - -/** - * Block macro to include snippets generated by Spring Rest Docs in a convenient way. - * Defaults to build (gradle) or target directory to look for the generated-snippets folder and its content. - * - * @author Gerrit Meier - */ -class RestDocsSnippetBlockMacro extends BlockMacroProcessor { - - private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; - private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String MAVEN_POM = "pom.xml"; - - public RestDocsSnippetBlockMacro(String macroName, Map config) { - super(macroName, config); - } - - @Override - protected Object process(AbstractBlock parent, String fileToInclude, Map attributes) { - String generatedSnippetPath = getDefaultOutputDirectory() + File.separator + fileToInclude; - - // since 'pass' context does not convert the content, we have to do this manually - String convertedContent = Asciidoctor.Factory.create().convertFile( - new File(generatedSnippetPath), - OptionsBuilder.options().toFile(false).inPlace(false).get()); - - return createBlock(parent, "pass", convertedContent, attributes, getConfig()); - } - - private static String getDefaultOutputDirectory() { - String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); - - if (Files.exists(Paths.get(MAVEN_POM))) { - return executingDirectory + File.separator + MAVEN_TARGET_PATH; - } - return executingDirectory + File.separator + GRADLE_BUILD_PATH; - } -} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry deleted file mode 100644 index 084eaa641..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry +++ /dev/null @@ -1 +0,0 @@ -org.springframework.restdocs.asciidoctor.extensions.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java deleted file mode 100644 index 9876af57f..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014-2016 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.asciidoctor.extensions; - -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.Options; -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link RestDocsSnippetBlockMacro}. - * - * @author Gerrit Meier - */ -public class RestDocsSnippetBlockMacroTest { - - @Before - public void prepareIncludeFiles() throws Exception { - Files.createDirectories(Paths.get("build/generated-snippets/")); - Files.copy(Paths.get("src/test/resources/rest_docs_macro.adoc"), - Paths.get("build/generated-snippets/rest_docs_macro.adoc"), StandardCopyOption.REPLACE_EXISTING); - } - - @Test - public void replaceRestDocsSnippetBlockWithFile() { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - asciidoctor.javaExtensionRegistry().blockMacro("snippet", RestDocsSnippetBlockMacro.class); - - assertThat(asciidoctor.convert("snippet::rest_docs_macro.adoc[]", new Options()), - equalTo("
            \n

            test text

            \n
            ")); - } - -} diff --git a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc deleted file mode 100644 index 276e7895a..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc +++ /dev/null @@ -1 +0,0 @@ -test text \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle new file mode 100644 index 000000000..6897242eb --- /dev/null +++ b/spring-restdocs-asciidoctor/build.gradle @@ -0,0 +1,7 @@ +description = 'Asciidoctor extensions for Spring REST Docs' + +dependencies { + compileOnly 'org.asciidoctor:asciidoctorj' + testCompile 'junit:junit' + testCompile 'org.asciidoctor:asciidoctorj' +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java new file mode 100644 index 000000000..7caa35f9c --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.Preprocessor; +import org.asciidoctor.extension.PreprocessorReader; + +/** + * {@link Preprocessor} that sets defaults for REST Docs-related {@link Document} + * attributes. + * + * @author Andy Wilkinson + */ +final class DefaultAttributesPreprocessor extends Preprocessor { + + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver( + new File(".")); + + @Override + public PreprocessorReader process(Document document, PreprocessorReader reader) { + document.setAttr("snippets", this.snippetsDirectoryResolver + .getSnippetsDirectory(document.getAttributes()), false); + return reader; + } + +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java similarity index 53% rename from spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 82a8e60bb..15bca1542 100644 --- a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -14,32 +14,22 @@ * limitations under the License. */ -package org.springframework.restdocs.asciidoctor.extensions; +package org.springframework.restdocs.asciidoctor; import org.asciidoctor.Asciidoctor; import org.asciidoctor.extension.spi.ExtensionRegistry; /** - * ExtensionRegistry for the Spring Rest Docs macros to get registered - * in all projects that include this asciidoctor extension module. - *

            - * Macros provided: - *

              - *
            • {@link RestDocsSnippetBlockMacro}
            • - *
            + * Asciidoctor {@link ExtensionRegistry} for Spring REST Docs. * - * @author Gerrit Meier + * @author Andy Wilkinson */ -public class RestDocsExtensionRegistry implements ExtensionRegistry { - - /** - * the name that identifies the block macro in the asciidoctor document - * (e.g.
            snippet::file_to_include[]
            ) - */ - private static final String SNIPPET_BLOCK_NAME = "snippet"; +public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.javaExtensionRegistry().blockMacro(SNIPPET_BLOCK_NAME, RestDocsSnippetBlockMacro.class); + asciidoctor.javaExtensionRegistry() + .preprocessor(new DefaultAttributesPreprocessor()); } + } diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java new file mode 100644 index 000000000..dd6829ade --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +/** + * Resolves the directory from which snippets can be read for inclusion in an Asciidoctor + * document. The resolved directory is relative to the {@code docdir} of the Asciidoctor + * document that it being rendered. + * + * @author Andy Wilkinson + */ +class SnippetsDirectoryResolver { + + private final File root; + + SnippetsDirectoryResolver(File root) { + this.root = root; + } + + File getSnippetsDirectory(Map attributes) { + if (new File(this.root, "pom.xml").exists()) { + return getMavenSnippetsDirectory(attributes); + } + return getGradleSnippetsDirectory(attributes); + } + + private File getMavenSnippetsDirectory(Map attributes) { + Path rootPath = Paths.get(this.root.getAbsolutePath()); + Path docDirPath = Paths.get((String) attributes.get("docdir")); + Path relativePath = docDirPath.relativize(rootPath); + return new File(relativePath.toFile(), "target/generated-snippets"); + } + + private File getGradleSnippetsDirectory(Map attributes) { + return new File((String) attributes.get("projectdir"), + "build/generated-snippets"); + } + +} diff --git a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..7c790a308 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java new file mode 100644 index 000000000..699bb49a6 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link DefaultAttributesPreprocessor}. + * + * @author Andy Wilkinson + */ +public class DefaultAttributesPreprocessorTests { + + @Test + public void snippetsAttributeIsSet() { + String converted = Asciidoctor.Factory.create().convert("{snippets}", + new Options()); + assertThat(converted, containsString("build/generated-snippets")); + } + + @Test + public void snippetsAttributeFromConvertArgumentIsNotOverridden() { + Options options = new Options(); + options.setAttributes(new Attributes("snippets=custom")); + String converted = Asciidoctor.Factory.create().convert("{snippets}", options); + assertThat(converted, containsString("custom")); + } + + @Test + public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { + Options options = new Options(); + options.setAttributes(new Attributes("snippets=custom")); + String converted = Asciidoctor.Factory.create() + .convert(":snippets: custom\n{snippets}", options); + assertThat(converted, containsString("custom")); + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java new file mode 100644 index 000000000..f998caf4d --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link SnippetsDirectoryResolver}. + * + * @author Andy Wilkinson + */ +public class SnippetsDirectoryResolverTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocDir() + throws IOException { + this.temporaryFolder.newFile("pom.xml"); + Map attributes = new HashMap<>(); + attributes.put("docdir", + new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") + .getAbsolutePath()); + File snippetsDirectory = new SnippetsDirectoryResolver( + this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + assertThat(snippetsDirectory.isAbsolute(), is(false)); + assertThat(snippetsDirectory, + equalTo(new File("../../../target/generated-snippets"))); + } + + @Test + public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectDir() + throws IOException { + Map attributes = new HashMap<>(); + attributes.put("projectdir", "project/dir"); + File snippetsDirectory = new SnippetsDirectoryResolver( + this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + assertThat(snippetsDirectory, + equalTo(new File("project/dir/build/generated-snippets"))); + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc new file mode 100644 index 000000000..0a3d1fa7e --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/' -i -H 'Accept: application/hal+json' +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index 19a8188c5..472832ff6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -17,8 +17,6 @@ package org.springframework.restdocs; import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; /** * {@code ManualRestDocumentation} is used to manually manage the @@ -33,11 +31,6 @@ */ public final class ManualRestDocumentation implements RestDocumentationContextProvider { - private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; - private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String MAVEN_POM = "pom.xml"; - private final File outputDirectory; private RestDocumentationContext context; @@ -57,7 +50,11 @@ public ManualRestDocumentation() { * @param outputDirectory the output directory */ public ManualRestDocumentation(String outputDirectory) { - this.outputDirectory = new File(outputDirectory); + this(new File(outputDirectory)); + } + + private ManualRestDocumentation(File outputDirectory) { + this.outputDirectory = outputDirectory; } /** @@ -94,12 +91,11 @@ public RestDocumentationContext beforeOperation() { return this.context; } - private static String getDefaultOutputDirectory() { - String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); - - if (Files.exists(Paths.get(MAVEN_POM))) { - return executingDirectory + File.separator + MAVEN_TARGET_PATH; + private static File getDefaultOutputDirectory() { + if (new File("pom.xml").exists()) { + return new File("target/generated-snippets"); } - return executingDirectory + File.separator + GRADLE_BUILD_PATH; + return new File("build/generated-snippets"); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0692cf61c..995a6ffb6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -195,7 +195,8 @@ public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidocto } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 7fa1d1c4d..83f96e611 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -83,7 +83,8 @@ private PlaceholderResolver createResolver(String methodName) { } private RestDocumentationContext createContext(String methodName) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); manualRestDocumentation.beforeTest(getClass(), methodName); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 49bdd4ccc..a4d3460d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -70,7 +70,8 @@ public void configuredOutputAndAbsoluteInput() { } private RestDocumentationContext createContext(String outputDir) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(outputDir); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + outputDir); manualRestDocumentation.beforeTest(getClass(), null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 154c59e23..698a80dac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -123,7 +123,8 @@ public Operation build() { } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(this.outputDirectory.getAbsolutePath()); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + this.outputDirectory.getAbsolutePath()); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; From 0c64c781f49e154c9dfaa76bd5569fad16154304 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:09:47 +0100 Subject: [PATCH 0344/1059] Improve styling of groovy and adoc code blocks in the documentation Closes gh-317 --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/docs/asciidoc/working-with-asciidoctor.adoc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0dd076206..8c5272998 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -145,7 +145,7 @@ the configuration are described below. <6> Using `prepare-package` allows the documentation to be <>. -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +[source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> @@ -235,7 +235,7 @@ Asciidoctor plugin to ensure that the documentation is generated before it's cop <3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. -[source,groovy,indent=0,role="secondary"] +[source,indent=0,role="secondary"] .Gradle ---- jar { diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index b3981dfeb..5ed3e9c09 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -22,7 +22,7 @@ macro] is used to include generated snippets in your documentation. The `snippet attribute specified in the <> can be used to reference the snippets output directory, for example: -[source,adoc,indent=0] +[source,indent=0] ---- \include::{snippets}/index/curl-request.adoc[] ---- @@ -45,7 +45,7 @@ Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For example, the widths of a table's columns can be specified using the `cols` attribute: -[source,adoc,indent=0] +[source,indent=0] ---- [cols=1,3] <1> \include::{snippets}/index/links.adoc[] @@ -60,7 +60,7 @@ three times as wide as the first. The title of a table can be specified using a line prefixed by a `.`: -[source,adoc,indent=0] +[source,indent=0] ---- .Links <1> \include::{snippets}/index/links.adoc[] From a2f1391fc88ed1aca77b06b56c51e75147294e70 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:10:47 +0100 Subject: [PATCH 0345/1059] Merge branch '1.1.x' --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/docs/asciidoc/working-with-asciidoctor.adoc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index a282bf46f..d70a78ce7 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -145,7 +145,7 @@ the configuration are described below. will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `target/generated-snippets`. -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +[source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> @@ -236,7 +236,7 @@ Asciidoctor plugin to ensure that the documentation is generated before it's cop <3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. -[source,groovy,indent=0,role="secondary"] +[source,indent=0,role="secondary"] .Gradle ---- jar { diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 283afcdf7..008ab6f98 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -23,7 +23,7 @@ attribute that is automatically set by `spring-restdocs-asciidoctor` configured <> can be used to reference the snippets output directory. For example: -[source,adoc,indent=0] +[source,indent=0] ---- \include::{snippets}/index/curl-request.adoc[] ---- @@ -46,7 +46,7 @@ Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For example, the widths of a table's columns can be specified using the `cols` attribute: -[source,adoc,indent=0] +[source,indent=0] ---- [cols=1,3] <1> \include::{snippets}/index/links.adoc[] @@ -61,7 +61,7 @@ three times as wide as the first. The title of a table can be specified using a line prefixed by a `.`: -[source,adoc,indent=0] +[source,indent=0] ---- .Links <1> \include::{snippets}/index/links.adoc[] From 2f568c21212a1b68cbc9825260f261c54eaca62b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:12:11 +0100 Subject: [PATCH 0346/1059] Install spring-restdocs-asciidoctor before building samples --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index fc664c7e3..7750213db 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,7 @@ samples { dependOn 'spring-restdocs-core:install' dependOn 'spring-restdocs-mockmvc:install' dependOn 'spring-restdocs-restassured:install' + dependOn 'spring-restdocs-asciidoctor:install' restNotesGrails { workingDir "$projectDir/samples/rest-notes-grails" From c13b7d03f5f6fa2d1b21e5ca6ff095a20b525415 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:26:18 +0100 Subject: [PATCH 0347/1059] Fix DefaultAttributesPreprocessorTests on Windows --- .../asciidoctor/DefaultAttributesPreprocessorTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 699bb49a6..8903c99e9 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.asciidoctor; +import java.io.File; + import org.asciidoctor.Asciidoctor; import org.asciidoctor.Attributes; import org.asciidoctor.Options; @@ -35,7 +37,8 @@ public class DefaultAttributesPreprocessorTests { public void snippetsAttributeIsSet() { String converted = Asciidoctor.Factory.create().convert("{snippets}", new Options()); - assertThat(converted, containsString("build/generated-snippets")); + assertThat(converted, + containsString("build" + File.separatorChar + "generated-snippets")); } @Test From 49596df0f3b59cd22329f70ff0b7f07ebc3ebf07 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 22 Oct 2016 21:58:11 +0100 Subject: [PATCH 0348/1059] Make snippet directory resolution more robust The previous approach had (at least) two problems: - A Gradle build run from a directory that also contains a pom.xml would result in the resolver incorrectly identifing that Maven was being used - A Maven build run from a directory that did not could a pom and that used -f to provide the path to a pom would result in the resolver incorretly indentifying that Gradle was being used With this commit, the resolver now uses the presence of the maven.home system property to identify that Maven is being used. When Maven is being used, rather than looking for a pom.xml in the working directory, the resolver now locates the pom.xml by searching up the directory hierarchy from the docdir. Closes gh-297 --- .../DefaultAttributesPreprocessor.java | 5 +- .../SnippetsDirectoryResolver.java | 38 ++++++++----- .../DefaultAttributesPreprocessorTests.java | 9 ++-- .../SnippetsDirectoryResolverTests.java | 53 +++++++++++++++++-- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java index 7caa35f9c..12ffd02ae 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.asciidoctor; -import java.io.File; - import org.asciidoctor.ast.Document; import org.asciidoctor.extension.Preprocessor; import org.asciidoctor.extension.PreprocessorReader; @@ -30,8 +28,7 @@ */ final class DefaultAttributesPreprocessor extends Preprocessor { - private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver( - new File(".")); + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); @Override public PreprocessorReader process(Document document, PreprocessorReader reader) { diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index dd6829ade..4e9c53eaf 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.asciidoctor; import java.io.File; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -30,29 +31,42 @@ */ class SnippetsDirectoryResolver { - private final File root; - - SnippetsDirectoryResolver(File root) { - this.root = root; - } - File getSnippetsDirectory(Map attributes) { - if (new File(this.root, "pom.xml").exists()) { + if (System.getProperty("maven.home") != null) { return getMavenSnippetsDirectory(attributes); } return getGradleSnippetsDirectory(attributes); } private File getMavenSnippetsDirectory(Map attributes) { - Path rootPath = Paths.get(this.root.getAbsolutePath()); - Path docDirPath = Paths.get((String) attributes.get("docdir")); - Path relativePath = docDirPath.relativize(rootPath); - return new File(relativePath.toFile(), "target/generated-snippets"); + Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir")); + return new File(docdir.relativize(findPom(docdir).getParent()).toFile(), + "target/generated-snippets"); + } + + private Path findPom(Path docdir) { + Path path = docdir; + while (path != null) { + Path pom = path.resolve("pom.xml"); + if (Files.isRegularFile(pom)) { + return pom; + } + path = path.getParent(); + } + throw new IllegalStateException("pom.xml not found in '" + docdir + "' or above"); } private File getGradleSnippetsDirectory(Map attributes) { - return new File((String) attributes.get("projectdir"), + return new File(getRequiredAttribute(attributes, "projectdir"), "build/generated-snippets"); } + private String getRequiredAttribute(Map attributes, String name) { + String attribute = (String) attributes.get(name); + if (attribute == null || attribute.length() == 0) { + throw new IllegalStateException(name + " attribute not found"); + } + return attribute; + } + } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 8903c99e9..83ef47824 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -35,8 +35,9 @@ public class DefaultAttributesPreprocessorTests { @Test public void snippetsAttributeIsSet() { - String converted = Asciidoctor.Factory.create().convert("{snippets}", - new Options()); + Options options = new Options(); + options.setAttributes(new Attributes("projectdir=../../..")); + String converted = Asciidoctor.Factory.create().convert("{snippets}", options); assertThat(converted, containsString("build" + File.separatorChar + "generated-snippets")); } @@ -44,7 +45,7 @@ public void snippetsAttributeIsSet() { @Test public void snippetsAttributeFromConvertArgumentIsNotOverridden() { Options options = new Options(); - options.setAttributes(new Attributes("snippets=custom")); + options.setAttributes(new Attributes("snippets=custom projectdir=../../..")); String converted = Asciidoctor.Factory.create().convert("{snippets}", options); assertThat(converted, containsString("custom")); } @@ -52,7 +53,7 @@ public void snippetsAttributeFromConvertArgumentIsNotOverridden() { @Test public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { Options options = new Options(); - options.setAttributes(new Attributes("snippets=custom")); + options.setAttributes(new Attributes("projectdir=../../..")); String converted = Asciidoctor.Factory.create() .convert(":snippets: custom\n{snippets}", options); assertThat(converted, containsString("custom")); diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java index f998caf4d..74bbda7e7 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import static org.hamcrest.CoreMatchers.equalTo; @@ -39,30 +40,72 @@ public class SnippetsDirectoryResolverTests { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test - public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocDir() + public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocdir() throws IOException { this.temporaryFolder.newFile("pom.xml"); Map attributes = new HashMap<>(); attributes.put("docdir", new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") .getAbsolutePath()); - File snippetsDirectory = new SnippetsDirectoryResolver( - this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + File snippetsDirectory = getMavenSnippetsDirectory(attributes); assertThat(snippetsDirectory.isAbsolute(), is(false)); assertThat(snippetsDirectory, equalTo(new File("../../../target/generated-snippets"))); } + @Test + public void illegalStateExceptionWhenMavenPomCannotBeFound() throws IOException { + Map attributes = new HashMap<>(); + String docdir = new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") + .getAbsolutePath(); + attributes.put("docdir", docdir); + this.thrown.expect(IllegalStateException.class); + this.thrown + .expectMessage(equalTo("pom.xml not found in '" + docdir + "' or above")); + getMavenSnippetsDirectory(attributes); + } + + @Test + public void illegalStateWhenDocdirAttributeIsNotSetInMavenProject() + throws IOException { + Map attributes = new HashMap<>(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage(equalTo("docdir attribute not found")); + getMavenSnippetsDirectory(attributes); + } + @Test public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectDir() throws IOException { Map attributes = new HashMap<>(); attributes.put("projectdir", "project/dir"); - File snippetsDirectory = new SnippetsDirectoryResolver( - this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + File snippetsDirectory = new SnippetsDirectoryResolver() + .getSnippetsDirectory(attributes); assertThat(snippetsDirectory, equalTo(new File("project/dir/build/generated-snippets"))); } + @Test + public void illegalStateWhenProjectdirAttributeIsNotSetInGradleProject() + throws IOException { + Map attributes = new HashMap<>(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage(equalTo("projectdir attribute not found")); + new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + } + + private File getMavenSnippetsDirectory(Map attributes) { + System.setProperty("maven.home", "/maven/home"); + try { + return new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + } + finally { + System.clearProperty("maven.home"); + } + } + } From 27b75538125f967098be5fd58f2fc9dd3ecd13ff Mon Sep 17 00:00:00 2001 From: Luis del Toro Date: Sat, 15 Oct 2016 16:33:14 +0200 Subject: [PATCH 0349/1059] Copy attributes when applying path prefix to field descriptors See gh-314 --- .../restdocs/payload/PayloadDocumentation.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 9a552c92e..49456a11a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.snippet.Attributes; + /** * Static factory methods for documenting a RESTful API's request and response payloads. * @@ -452,6 +454,7 @@ public static List applyPathPrefix(String pathPrefix, pathPrefix + descriptor.getPath()) .description(descriptor.getDescription()) .type(descriptor.getType()); + prefixedDescriptor.attributes(convertAttributesToArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } @@ -463,4 +466,12 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } + private static Attributes.Attribute[] convertAttributesToArray(Map attributes) { + List attributesAsList = new ArrayList<>(); + for (Map.Entry entry : attributes.entrySet()) { + attributesAsList.add(Attributes.key(entry.getKey()).value(entry.getValue())); + } + return attributesAsList.toArray(new Attributes.Attribute[] {}); + } + } From 7487f413631544087a50aedea0bb21a024c79b88 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 11:25:14 +0100 Subject: [PATCH 0350/1059] Polish "Copy attributes when applying path prefix to field descriptors" - Apply code formatting - Test copying of attributes (and other parts of a field descriptor) in new PayloadDocumentationTests Closes gh-314 --- .../payload/PayloadDocumentation.java | 16 ++-- .../payload/PayloadDocumentationTests.java | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 49456a11a..c4a57c254 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.restdocs.snippet.Attributes; +import org.springframework.restdocs.snippet.Attributes.Attribute; /** * Static factory methods for documenting a RESTful API's request and response payloads. @@ -453,8 +454,8 @@ public static List applyPathPrefix(String pathPrefix, FieldDescriptor prefixedDescriptor = new FieldDescriptor( pathPrefix + descriptor.getPath()) .description(descriptor.getDescription()) - .type(descriptor.getType()); - prefixedDescriptor.attributes(convertAttributesToArray(descriptor.getAttributes())); + .type(descriptor.getType()) + .attributes(asArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } @@ -466,12 +467,13 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } - private static Attributes.Attribute[] convertAttributesToArray(Map attributes) { - List attributesAsList = new ArrayList<>(); - for (Map.Entry entry : attributes.entrySet()) { - attributesAsList.add(Attributes.key(entry.getKey()).value(entry.getValue())); + private static Attribute[] asArray(Map attributeMap) { + List attributes = new ArrayList<>(); + for (Map.Entry attribute : attributeMap.entrySet()) { + attributes + .add(Attributes.key(attribute.getKey()).value(attribute.getValue())); } - return attributesAsList.toArray(new Attributes.Attribute[] {}); + return attributes.toArray(new Attribute[attributes.size()]); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java new file mode 100644 index 000000000..e17671aa2 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014-2016 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.Arrays; +import java.util.List; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.applyPathPrefix; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link PayloadDocumentation}. + * + * @author Andy Wilkinson + */ +public class PayloadDocumentationTests { + + @Test + public void applyPathPrefixAppliesPrefixToDescriptorPaths() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo"), fieldWithPath("charlie"))); + assertThat(descriptors.size(), is(equalTo(2))); + assertThat(descriptors.get(0).getPath(), is(equalTo("alpha.bravo"))); + } + + @Test + public void applyPathPrefixCopiesIgnored() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").ignored())); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).isIgnored(), is(true)); + } + + @Test + public void applyPathPrefixCopiesOptional() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").optional())); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).isOptional(), is(true)); + } + + @Test + public void applyPathPrefixCopiesDescription() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").description("Some field"))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getDescription(), + is(equalTo((Object) "Some field"))); + } + + @Test + public void applyPathPrefixCopiesType() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").type(JsonFieldType.OBJECT))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getType(), + is(equalTo((Object) JsonFieldType.OBJECT))); + } + + @Test + public void applyPathPrefixCopiesAttributes() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").attributes(key("a").value("alpha"), + key("b").value("bravo")))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getAttributes().size(), is(equalTo(2))); + assertThat(descriptors.get(0).getAttributes().get("a"), + is(equalTo((Object) "alpha"))); + assertThat(descriptors.get(0).getAttributes().get("b"), + is(equalTo((Object) "bravo"))); + } + +} From 4f19294220eea0ed442f9d3d0718f7135ff11981 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 12:27:03 +0100 Subject: [PATCH 0351/1059] Allow a TemplatedSnippet to produce multiple snippets from same template Previously a TemplatedSnippet could only write snippets with the same name as its template. This meant that the same template could not be used to produce multiple snippets with different names. This commit separates the snippet name from the template name, thereby allowing the snippet name to vary while the template name remains the same. For backwards compatibility, the default behaviour is for the template name and snippet name to remain the same. The new overloaded constructor can be used when the names need to differ. Closes gh-320 --- .../restdocs/snippet/TemplatedSnippet.java | 28 ++- .../restdocs/AbstractSnippetTests.java | 6 +- .../restdocs/cli/CurlRequestSnippetTests.java | 60 +++--- .../cli/HttpieRequestSnippetTests.java | 60 +++--- .../RequestHeadersSnippetFailureTests.java | 4 +- .../headers/RequestHeadersSnippetTests.java | 14 +- .../ResponseHeadersSnippetFailureTests.java | 4 +- .../headers/ResponseHeadersSnippetTests.java | 14 +- .../http/HttpRequestSnippetTests.java | 46 ++--- .../http/HttpResponseSnippetTests.java | 14 +- .../hypermedia/LinksSnippetFailureTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 20 +- .../AsciidoctorRequestFieldsSnippetTests.java | 6 +- .../RequestFieldsSnippetFailureTests.java | 5 +- .../payload/RequestFieldsSnippetTests.java | 30 +-- .../ResponseFieldsSnippetFailureTests.java | 4 +- .../payload/ResponseFieldsSnippetTests.java | 38 ++-- .../PathParametersSnippetFailureTests.java | 4 +- .../request/PathParametersSnippetTests.java | 22 +-- .../RequestParametersSnippetFailureTests.java | 4 +- .../RequestParametersSnippetTests.java | 22 +-- .../RequestPartsSnippetFailureTests.java | 4 +- .../request/RequestPartsSnippetTests.java | 20 +- .../snippet/TemplatedSnippetTests.java | 29 ++- .../restdocs/test/ExpectedSnippet.java | 162 ---------------- .../restdocs/test/ExpectedSnippets.java | 177 ++++++++++++++++++ .../templates/multiple-snippets.snippet | 0 27 files changed, 432 insertions(+), 369 deletions(-) delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index dedfa98a3..cfb584fb6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -38,15 +38,34 @@ public abstract class TemplatedSnippet implements Snippet { private final String snippetName; + private final String templateName; + /** * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given - * {@code snippetName}. The given {@code attributes} will be included in the model - * during rendering of the template. + * {@code snippetName}. The {@code snippetName} will also be used as the name of the + * template. The given {@code attributes} will be included in the model during + * rendering of the template. * * @param snippetName The name of the snippet * @param attributes The additional attributes + * @see #TemplatedSnippet(String, String, Map) */ protected TemplatedSnippet(String snippetName, Map attributes) { + this(snippetName, snippetName, attributes); + } + + /** + * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given + * {@code snippetName} using a template with the given {@code templateName}. The given + * {@code attributes} will be included in the model during rendering of the template. + * + * @param snippetName The name of the snippet + * @param templateName The name of the template + * @param attributes The additional attributes + */ + protected TemplatedSnippet(String snippetName, String templateName, + Map attributes) { + this.templateName = templateName; this.snippetName = snippetName; if (attributes != null) { this.attributes.putAll(attributes); @@ -65,7 +84,8 @@ public void document(Operation operation) throws IOException { model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() .get(TemplateEngine.class.getName()); - writer.append(templateEngine.compileTemplate(this.snippetName).render(model)); + writer.append( + templateEngine.compileTemplate(this.templateName).render(model)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 41b18ffda..dbca4c0e9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -27,7 +27,7 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import org.springframework.restdocs.test.SnippetMatchers; import org.springframework.restdocs.test.SnippetMatchers.CodeBlockMatcher; @@ -50,7 +50,7 @@ public abstract class AbstractSnippetTests { protected final TemplateFormat templateFormat; @Rule - public ExpectedSnippet snippet; + public ExpectedSnippets snippets; @Rule public OperationBuilder operationBuilder; @@ -62,7 +62,7 @@ public static List parameters() { } public AbstractSnippetTests(String name, TemplateFormat templateFormat) { - this.snippet = new ExpectedSnippet(templateFormat); + this.snippets = new ExpectedSnippets(templateFormat); this.templateFormat = templateFormat; this.operationBuilder = new OperationBuilder(this.templateFormat); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index c9e2ead72..fc4cf64c7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -55,7 +55,7 @@ public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo").build()); @@ -63,7 +63,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); @@ -71,7 +71,7 @@ public void getRequestWithParameter() throws IOException { @Test public void nonGetRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); @@ -79,7 +79,7 @@ public void nonGetRequest() throws IOException { @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); @@ -87,7 +87,7 @@ public void requestWithContent() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); @@ -96,7 +96,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param=value") @@ -106,7 +106,7 @@ public void getRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -115,7 +115,7 @@ public void getRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); @@ -123,7 +123,7 @@ public void getRequestWithDisjointQueryStringAndParameters() throws IOException @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?param' -i")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param").build()); @@ -131,7 +131,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").method("POST").build()); @@ -139,7 +139,7 @@ public void postRequestWithQueryString() throws IOException { @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param").method("POST").build()); @@ -147,7 +147,7 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -156,7 +156,7 @@ public void postRequestWithOneParameter() throws IOException { @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("k1").build()); @@ -164,7 +164,7 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); @@ -175,7 +175,7 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -184,7 +184,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + this.snippets.expectCurlRequest().withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -194,7 +194,7 @@ public void postRequestWithDisjointQueryStringAndParameter() throws IOException @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") @@ -204,7 +204,7 @@ public void postRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + this.snippets.expectCurlRequest().withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -213,7 +213,7 @@ public void postRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); @@ -221,7 +221,7 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); @@ -232,7 +232,7 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -241,7 +241,7 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); new CurlRequestSnippet() @@ -256,7 +256,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/upload").method("POST") @@ -269,7 +269,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -285,7 +285,7 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -301,7 +301,7 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -314,7 +314,7 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -326,7 +326,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) @@ -344,7 +344,7 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + " -H 'Content-Type: application/json' -H 'a: alpha'")); @@ -358,7 +358,7 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X POST -d 'Some content'")); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 527a2c7f8..d5dc8a399 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -56,7 +56,7 @@ public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo").build()); @@ -64,7 +64,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); @@ -72,7 +72,7 @@ public void getRequestWithParameter() throws IOException { @Test public void nonGetRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); @@ -80,7 +80,7 @@ public void nonGetRequest() throws IOException { @Test public void requestWithContent() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ echo 'content' | http GET 'http://localhost/foo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); @@ -88,7 +88,7 @@ public void requestWithContent() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); @@ -97,7 +97,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param=value") @@ -107,7 +107,7 @@ public void getRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -116,7 +116,7 @@ public void getRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); @@ -124,7 +124,7 @@ public void getRequestWithDisjointQueryStringAndParameters() throws IOException @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?param'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param").build()); @@ -132,7 +132,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http POST 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").method("POST").build()); @@ -140,7 +140,7 @@ public void postRequestWithQueryString() throws IOException { @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo?param'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param").method("POST").build()); @@ -148,7 +148,7 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -157,7 +157,7 @@ public void postRequestWithOneParameter() throws IOException { @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1='")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("k1").build()); @@ -165,7 +165,7 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); @@ -176,7 +176,7 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -185,7 +185,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -195,7 +195,7 @@ public void postRequestWithDisjointQueryStringAndParameter() throws IOException @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") @@ -205,7 +205,7 @@ public void postRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -214,7 +214,7 @@ public void postRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); @@ -222,7 +222,7 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); @@ -233,7 +233,7 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -242,7 +242,7 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); new HttpieRequestSnippet() @@ -257,7 +257,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/upload").method("POST") @@ -271,7 +271,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -287,7 +287,7 @@ public void multipartPost() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -303,7 +303,7 @@ public void multipartPostWithParameters() throws IOException { .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + " 'b=banana'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -316,7 +316,7 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -328,7 +328,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(containsString("httpie request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("httpie-request")) @@ -346,7 +346,7 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content( "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + " 'Content-Type:application/json' 'a:alpha'")); @@ -360,7 +360,7 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ echo 'Some content' | http POST " + "'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document(this.operationBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index f1192567c..e2eba481a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -44,7 +44,7 @@ public class RequestHeadersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 373e83b70..ef87c2f72 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -49,7 +49,7 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders() + this.snippets.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -83,7 +83,7 @@ public void requestWithHeaders() throws IOException { @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -93,7 +93,7 @@ public void caseInsensitiveRequestHeaders() throws IOException { @Test public void undocumentedRequestHeader() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -104,7 +104,7 @@ public void undocumentedRequestHeader() throws IOException { @Test public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(containsString("Custom title")); + this.snippets.expectRequestHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-title")); @@ -123,7 +123,7 @@ public void requestHeadersWithCustomAttributes() throws IOException { @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(// + this.snippets.expectRequestHeaders().withContents(// tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha") .row("Accept-Encoding", "two", "bravo") @@ -152,7 +152,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestHeaders() + this.snippets.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -178,7 +178,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new RequestHeadersSnippet( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index e42d38179..72d38fccb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -44,7 +44,7 @@ public class ResponseHeadersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index ba572598d..7f5fe1d64 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -49,7 +49,7 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -71,7 +71,7 @@ public void responseWithHeaders() throws IOException { @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -81,7 +81,7 @@ public void caseInsensitiveResponseHeaders() throws IOException { @Test public void undocumentedResponseHeader() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))).document( @@ -91,7 +91,7 @@ public void undocumentedResponseHeader() throws IOException { @Test public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders().withContents(containsString("Custom title")); + this.snippets.expectResponseHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")) .willReturn(snippetResource("response-headers-with-title")); @@ -110,7 +110,7 @@ public void responseHeadersWithCustomAttributes() throws IOException { @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseHeaders() + this.snippets.expectResponseHeaders() .withContents(tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); @@ -138,7 +138,7 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -157,7 +157,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new ResponseHeadersSnippet( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 6ba4d65a3..88c27ee1e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -51,7 +51,7 @@ public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost")); @@ -61,7 +61,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); @@ -72,7 +72,7 @@ public void getRequestWithParameters() throws IOException { @Test public void getRequestWithPort() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost:8080")); @@ -82,7 +82,7 @@ public void getRequestWithPort() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") .header(HttpHeaders.HOST, "localhost")); @@ -92,7 +92,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); @@ -102,7 +102,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); @@ -113,7 +113,7 @@ public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOExcep @Test public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); @@ -125,7 +125,7 @@ public void getWithTotallyOverlappingQueryStringAndParameters() throws IOExcepti @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -137,7 +137,7 @@ public void postRequestWithContent() throws IOException { @Test public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -151,7 +151,7 @@ public void postRequestWithContentAndParameters() throws IOException { public void postRequestWithContentAndDisjointQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -165,7 +165,7 @@ public void postRequestWithContentAndDisjointQueryStringAndParameters() public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -179,7 +179,7 @@ public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameter public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -193,7 +193,7 @@ public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters( public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") @@ -208,7 +208,7 @@ public void postRequestWithCharset() throws IOException { @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -221,7 +221,7 @@ public void postRequestWithParameter() throws IOException { @Test public void postRequestWithParameterWithNoValue() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -234,7 +234,7 @@ public void postRequestWithParameterWithNoValue() throws IOException { @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -245,7 +245,7 @@ public void putRequestWithContent() throws IOException { @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -260,7 +260,7 @@ public void putRequestWithParameter() throws IOException { public void multipartPost() throws IOException { String expectedContent = createPart(String.format( "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -286,7 +286,7 @@ public void multipartPostWithParameters() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -306,7 +306,7 @@ public void multipartPostWithParameterWithNoValue() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = paramPart + filePart; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -323,7 +323,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -339,7 +339,7 @@ public void multipartPostWithContentType() throws IOException { @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo") .header(HttpHeaders.HOST, "api.example.com")); new HttpRequestSnippet() @@ -349,7 +349,7 @@ public void getRequestWithCustomHost() throws IOException { @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 8560fd399..bbfa255ab 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -49,13 +49,13 @@ public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); + this.snippets.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); new HttpResponseSnippet().document(this.operationBuilder.build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.BAD_REQUEST)); new HttpResponseSnippet().document(this.operationBuilder.response() .status(HttpStatus.BAD_REQUEST.value()).build()); @@ -63,7 +63,7 @@ public void nonOkResponse() throws IOException { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) + this.snippets.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet() .document(this.operationBuilder.response() @@ -75,7 +75,7 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK).content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet() @@ -86,7 +86,7 @@ public void responseWithContent() throws IOException { public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) @@ -98,7 +98,7 @@ public void responseWithCharset() throws IOException { @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index f8e0cf762..f8d9c6a04 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,7 +43,7 @@ public class LinksSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 3236bd8ec..7f6f98d32 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,7 +47,7 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -59,7 +59,7 @@ public void ignoredLink() throws IOException { @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -70,7 +70,7 @@ public void allUndocumentedLinksCanBeIgnored() throws IOException { @Test public void presentOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) @@ -79,7 +79,7 @@ public void presentOptionalLink() throws IOException { @Test public void missingOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) @@ -88,7 +88,7 @@ public void missingOptionalLink() throws IOException { @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "two")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -100,7 +100,7 @@ public void documentedLinks() throws IOException { @Test public void linkDescriptionFromTitleInPayload() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), @@ -114,7 +114,7 @@ public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-title")); - this.snippet.expectLinks().withContents(containsString("Title for the links")); + this.snippets.expectLinks().withContents(containsString("Title for the links")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -134,7 +134,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-extra-column")); - this.snippet.expectLinks() + this.snippets.expectLinks() .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -154,7 +154,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -166,7 +166,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index d1a83a662..9f737e3a7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -26,7 +26,7 @@ import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.mockito.BDDMockito.given; @@ -46,14 +46,14 @@ public class AsciidoctorRequestFieldsSnippetTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Test public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields().withContents( + this.snippets.expectRequestFields().withContents( tableWithHeader(asciidoctor(), "Path", "Type", "Description") // .row("a", "String", String.format(" - one%n - two")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 30d16443a..d6fd39986 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -28,7 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -45,7 +45,8 @@ public class RequestFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets( + TemplateFormats.asciidoctor()); @Rule public OperationBuilder operationBuilder = new OperationBuilder( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 8970a43da..8e6d8414d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -50,7 +50,7 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -65,7 +65,7 @@ public void mapRequestWithFields() throws IOException { @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two") .row("`[]a.c`", "`String`", "three") @@ -83,7 +83,7 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -95,7 +95,7 @@ public void ignoredRequestField() throws IOException { @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), @@ -106,7 +106,7 @@ public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -118,7 +118,7 @@ public void missingOptionalRequestField() throws IOException { @Test public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) @@ -128,7 +128,7 @@ public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() @Test public void presentOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -142,7 +142,7 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields().withContents(containsString("Custom title")); + this.snippets.expectRequestFields().withContents(containsString("Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes( @@ -161,7 +161,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-extra-column")); - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -187,7 +187,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); @@ -199,7 +199,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); @@ -211,7 +211,7 @@ public void fieldWithExplictVariesType() throws IOException { @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -230,7 +230,7 @@ public void xmlRequestFields() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -245,7 +245,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -259,7 +259,7 @@ public void prefixedAdditionalDescriptors() throws IOException { @Test public void requestWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 05b1b9c17..92c8da49a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -27,7 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -48,7 +48,7 @@ public class ResponseFieldsSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index d22fdb332..59002896a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -50,7 +50,7 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -72,7 +72,7 @@ public void mapResponseWithFields() throws IOException { @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") @@ -88,7 +88,7 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) @@ -98,7 +98,7 @@ public void arrayResponse() throws IOException { @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -110,7 +110,7 @@ public void ignoredResponseField() throws IOException { @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -125,7 +125,7 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields().withContents(containsString("Custom title")); + this.snippets.expectResponseFields().withContents(containsString("Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes( @@ -141,7 +141,7 @@ public void responseFieldsWithCustomAttributes() throws IOException { @Test public void missingOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -152,7 +152,7 @@ public void missingOptionalResponseField() throws IOException { @Test public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) @@ -161,7 +161,7 @@ public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() @Test public void presentOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -175,7 +175,7 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-extra-column")); - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -201,7 +201,7 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); @@ -213,7 +213,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); @@ -225,7 +225,7 @@ public void fieldWithExplictVariesType() throws IOException { @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -243,7 +243,7 @@ public void xmlResponseFields() throws IOException { @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( @@ -259,7 +259,7 @@ public void xmlAttribute() throws IOException { @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( @@ -275,7 +275,7 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { - this.snippet.expectResponseFields().withContents( + this.snippets.expectResponseFields().withContents( tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("a"))).document( @@ -287,7 +287,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -309,7 +309,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -323,7 +323,7 @@ public void prefixedAdditionalDescriptors() throws IOException { @Test public void responseWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 26f0f6b09..27a897d5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -45,7 +45,7 @@ public class PathParametersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c74f5d401..b16a8a95f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -49,7 +49,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -63,7 +63,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), @@ -77,7 +77,7 @@ public void ignoredPathParameter() throws IOException { @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( @@ -89,7 +89,7 @@ public void allUndocumentedPathParametersCanBeIgnored() throws IOException { @Test public void missingOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.snippets.expectPathParameters().withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -101,7 +101,7 @@ public void missingOptionalPathParameter() throws IOException { @Test public void presentOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.snippets.expectPathParameters().withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( @@ -113,7 +113,7 @@ public void presentOptionalPathParameter() throws IOException { @Test public void pathParametersWithQueryString() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -127,7 +127,7 @@ public void pathParametersWithQueryString() throws IOException { @Test public void pathParametersWithQueryStringWithParameters() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -144,7 +144,7 @@ public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters().withContents(containsString("The title")); + this.snippets.expectPathParameters().withContents(containsString("The title")); new PathParametersSnippet( Arrays.asList( @@ -168,7 +168,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters() + this.snippets.expectPathParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -188,7 +188,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) @@ -201,7 +201,7 @@ public void additionalDescriptors() throws IOException { @Test public void pathParametersWithEscapedContent() throws IOException { - this.snippet.expectPathParameters() + this.snippets.expectPathParameters() .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index ce50cc892..a979f65ef 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -44,7 +44,7 @@ public class RequestParametersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index cffbb671a..ce2cc4d4a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,7 +48,7 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( @@ -60,7 +60,7 @@ public void requestParameters() throws IOException { @Test public void requestParameterWithNoValue() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) @@ -70,7 +70,7 @@ public void requestParameterWithNoValue() throws IOException { @Test public void ignoredRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -80,7 +80,7 @@ public void ignoredRequestParameter() throws IOException { @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) @@ -90,7 +90,7 @@ public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { @Test public void missingOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( @@ -102,7 +102,7 @@ public void missingOptionalRequestParameter() throws IOException { @Test public void presentOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) @@ -115,7 +115,7 @@ public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters().withContents(containsString("The title")); + this.snippets.expectRequestParameters().withContents(containsString("The title")); new RequestParametersSnippet( Arrays.asList( @@ -140,7 +140,7 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -164,7 +164,7 @@ public void requestParametersWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-optional-column")); - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); @@ -183,7 +183,7 @@ public void requestParametersWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) @@ -194,7 +194,7 @@ public void additionalDescriptors() throws IOException { @Test public void requestParametersWithEscapedContent() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index 522598201..935e2d02c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -44,7 +44,7 @@ public class RequestPartsSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index ecdce573a..f49d299a5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -48,7 +48,7 @@ public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestParts() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), @@ -60,7 +60,7 @@ public void requestParts() throws IOException { @Test public void ignoredRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) @@ -71,7 +71,7 @@ public void ignoredRequestPart() throws IOException { @Test public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) .document(this.operationBuilder.request("http://localhost") @@ -81,7 +81,7 @@ public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet( @@ -93,7 +93,7 @@ public void missingOptionalRequestPart() throws IOException { @Test public void presentOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional())) @@ -106,7 +106,7 @@ public void requestPartsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-title")); - this.snippet.expectRequestParts().withContents(containsString("The title")); + this.snippets.expectRequestParts().withContents(containsString("The title")); new RequestPartsSnippet( Arrays.asList( @@ -131,7 +131,7 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-extra-column")); - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -155,7 +155,7 @@ public void requestPartsWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-optional-column")); - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); @@ -174,7 +174,7 @@ public void requestPartsWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); RequestDocumentation.requestParts(partWithName("a").description("one")) @@ -186,7 +186,7 @@ public void additionalDescriptors() throws IOException { @Test public void requestPartsWithEscapedContent() throws IOException { - this.snippet.expectRequestParts().withContents( + this.snippets.expectRequestParts().withContents( tableWithHeader("Part", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index f64034c67..aea28d3ce 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -16,13 +16,18 @@ package org.springframework.restdocs.snippet; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.junit.Rule; import org.junit.Test; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippets; +import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -38,6 +43,13 @@ */ public class TemplatedSnippetTests { + @Rule + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); + + @Rule + public ExpectedSnippets snippets = new ExpectedSnippets(TemplateFormats.asciidoctor()); + @Test public void attributesAreCopied() { Map attributes = new HashMap<>(); @@ -60,15 +72,30 @@ public void snippetName() { .getSnippetName(), is(equalTo("test"))); } + @Test + public void multipleSnippetsCanBeProducedFromTheSameTemplate() throws IOException { + this.snippets.expect("multiple-snippets-one"); + this.snippets.expect("multiple-snippets-two"); + new TestTemplatedSnippet("one", "multiple-snippets") + .document(this.operationBuilder.build()); + new TestTemplatedSnippet("two", "multiple-snippets") + .document(this.operationBuilder.build()); + } + private static class TestTemplatedSnippet extends TemplatedSnippet { + protected TestTemplatedSnippet(String snippetName, String templateName) { + super(templateName + "-" + snippetName, templateName, + Collections.emptyMap()); + } + protected TestTemplatedSnippet(Map attributes) { super("test", attributes); } @Override protected Map createModel(Operation operation) { - return null; + return new HashMap<>(); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java deleted file mode 100644 index 8036e98e0..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ /dev/null @@ -1,162 +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.test; - -import java.io.File; -import java.io.IOException; - -import org.hamcrest.Matcher; -import org.junit.runners.model.Statement; - -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -/** - * The {@code ExpectedSnippet} rule is used to verify that a {@link TemplatedSnippet} has - * generated the expected snippet. - * - * @author Andy Wilkinson - * @author Andreas Evers - */ -public class ExpectedSnippet extends OperationTestRule { - - private final TemplateFormat templateFormat; - - private final SnippetMatcher snippet; - - private String expectedName; - - private String expectedType; - - private File outputDirectory; - - public ExpectedSnippet(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - this.snippet = SnippetMatchers.snippet(templateFormat); - } - - @Override - public Statement apply(final Statement base, File outputDirectory, - String operationName) { - this.outputDirectory = outputDirectory; - this.expectedName = operationName; - return new ExpectedSnippetStatement(base); - } - - private void verifySnippet() throws IOException { - if (this.outputDirectory != null && this.expectedName != null) { - File snippetDir = new File(this.outputDirectory, this.expectedName); - File snippetFile = new File(snippetDir, - this.expectedType + "." + this.templateFormat.getFileExtension()); - assertThat(snippetFile, is(this.snippet)); - } - } - - public ExpectedSnippet expectCurlRequest() { - expect("curl-request"); - return this; - } - - public ExpectedSnippet expectHttpieRequest() { - expect("httpie-request"); - return this; - } - - public ExpectedSnippet expectRequestFields() { - expect("request-fields"); - return this; - } - - public ExpectedSnippet expectResponseFields() { - expect("response-fields"); - return this; - } - - public ExpectedSnippet expectRequestHeaders() { - expect("request-headers"); - return this; - } - - public ExpectedSnippet expectResponseHeaders() { - expect("response-headers"); - return this; - } - - public ExpectedSnippet expectLinks() { - expect("links"); - return this; - } - - public ExpectedSnippet expectHttpRequest() { - expect("http-request"); - return this; - } - - public ExpectedSnippet expectHttpResponse() { - expect("http-response"); - return this; - } - - public ExpectedSnippet expectRequestParameters() { - expect("request-parameters"); - return this; - } - - public ExpectedSnippet expectPathParameters() { - expect("path-parameters"); - return this; - } - - public ExpectedSnippet expectRequestParts() { - expect("request-parts"); - return this; - } - - private ExpectedSnippet expect(String type) { - this.expectedType = type; - return this; - } - - public void withContents(Matcher matcher) { - this.snippet.withContents(matcher); - } - - public File getOutputDirectory() { - return this.outputDirectory; - } - - private final class ExpectedSnippetStatement extends Statement { - - private final Statement delegate; - - private ExpectedSnippetStatement(Statement delegate) { - this.delegate = delegate; - } - - @Override - public void evaluate() throws Throwable { - this.delegate.evaluate(); - verifySnippet(); - } - - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java new file mode 100644 index 000000000..f57c5b959 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014-2016 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.test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.runners.model.Statement; + +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * The {@code ExpectedSnippets} rule is used to verify that a test of a + * {@link TemplatedSnippet} has generated the expected snippets. + * + * @author Andy Wilkinson + * @author Andreas Evers + */ +public class ExpectedSnippets extends OperationTestRule { + + private final TemplateFormat templateFormat; + + private String operationName; + + private File outputDirectory; + + private List expectations = new ArrayList<>(); + + public ExpectedSnippets(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public Statement apply(final Statement base, File outputDirectory, + String operationName) { + this.outputDirectory = outputDirectory; + this.operationName = operationName; + return new ExpectedSnippetsStatement(base); + } + + private void verifySnippets() throws IOException { + if (this.outputDirectory != null && this.operationName != null) { + File snippetDir = new File(this.outputDirectory, this.operationName); + for (ExpectedSnippet expectation : this.expectations) { + expectation.verify(snippetDir); + } + } + } + + public ExpectedSnippet expectCurlRequest() { + return expect("curl-request"); + } + + public ExpectedSnippet expectHttpieRequest() { + return expect("httpie-request"); + } + + public ExpectedSnippet expectRequestFields() { + return expect("request-fields"); + } + + public ExpectedSnippet expectResponseFields() { + return expect("response-fields"); + } + + public ExpectedSnippet expectRequestHeaders() { + return expect("request-headers"); + } + + public ExpectedSnippet expectResponseHeaders() { + return expect("response-headers"); + } + + public ExpectedSnippet expectLinks() { + return expect("links"); + } + + public ExpectedSnippet expectHttpRequest() { + return expect("http-request"); + } + + public ExpectedSnippet expectHttpResponse() { + return expect("http-response"); + } + + public ExpectedSnippet expectRequestParameters() { + return expect("request-parameters"); + } + + public ExpectedSnippet expectPathParameters() { + return expect("path-parameters"); + } + + public ExpectedSnippet expectRequestParts() { + return expect("request-parts"); + } + + public ExpectedSnippet expect(String type) { + ExpectedSnippet expectedSnippet = new ExpectedSnippet( + SnippetMatchers.snippet(this.templateFormat), type); + this.expectations.add(expectedSnippet); + return expectedSnippet; + } + + public File getOutputDirectory() { + return this.outputDirectory; + } + + public String getOperationName() { + return this.operationName; + } + + /** + * Expecations for a particular snippet. + */ + public final class ExpectedSnippet { + + private final SnippetMatcher snippetMatcher; + + private final String snippetName; + + private ExpectedSnippet(SnippetMatcher snippetMatcher, String snippetName) { + this.snippetMatcher = snippetMatcher; + this.snippetName = snippetName; + } + + private void verify(File snippetDir) { + File snippetFile = new File(snippetDir, this.snippetName + "." + + ExpectedSnippets.this.templateFormat.getFileExtension()); + assertThat(snippetFile, is(this.snippetMatcher)); + } + + public void withContents(Matcher matcher) { + this.snippetMatcher.withContents(matcher); + } + + } + + private final class ExpectedSnippetsStatement extends Statement { + + private final Statement delegate; + + private ExpectedSnippetsStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + this.delegate.evaluate(); + verifySnippets(); + } + + } + +} diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet new file mode 100644 index 000000000..e69de29bb From 13e745a9a93e018b95285de2b3f8e3d5f81a6e90 Mon Sep 17 00:00:00 2001 From: Mathieu POUSSE Date: Thu, 23 Jun 2016 17:28:26 +0200 Subject: [PATCH 0352/1059] Add support for documenting request part payload fields See gh-270 --- .../docs/asciidoc/documenting-your-api.adoc | 6 + .../payload/PayloadDocumentation.java | 50 +++++ .../payload/RequestPartFieldsSnippet.java | 191 ++++++++++++++++++ ...RequestPartsFieldsSnippetFailureTests.java | 82 ++++++++ .../RequestPartsFieldsSnippetTests.java | 56 +++++ 5 files changed, 385 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442b..96117ffda 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -575,6 +575,12 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. +[[documenting-your-api-request-parts]] +=== Request parts content + +If you need to document the content of request part that holds Json data, you should use`requestPartsFields`. +It acts exactly as [[documenting-your-api-request-parts]] but in order to use it, you must specify the part name. + [[documenting-your-api-http-headers]] === HTTP headers diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index c4a57c254..fed726f1d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -148,6 +148,56 @@ public static RequestFieldsSnippet requestFields(List descripto return new RequestFieldsSnippet(descriptors); } + /** + * Returns a {@code Snippet} that will document the fields of the specified {@code part} + * of the API operations's request payload. The fields will be documented using the + * given {@code descriptors}. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified {@code part} + * of the API operations's request payload. The fields will be documented using the given + * {@code descriptors}. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java new file mode 100644 index 000000000..4de8f8486 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -0,0 +1,191 @@ +/* + * Copyright 2014-2016 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.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the fields in a request. + * + * @author Mathieu Pousse + * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + */ +public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. Undocumented fields will trigger a + * failure. + * + * @param partName the part name + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, List descriptors) { + this(partName, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + boolean ignoreUndocumentedFields) { + this(partName, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented fields will trigger a + * failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes) { + this(partName, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields); + this.partName = partName; + } + + @Override + protected MediaType getContentType(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getHeaders().getContentType(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getContent(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + /** + * Prepare the error message because the requested part was not found. + * + * @return see description + */ + protected String missingPartErrorMessage() { + return "Request parts with the following names were not found in the request: " + this.partName; + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(List additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java new file mode 100644 index 000000000..a025514ce --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2016 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.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link RequestPartFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Mathieu Pousse + */ +public class RequestPartsFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new RequestPartFieldsSnippet("part", Collections.emptyList()) + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were not found in the request: another")); + new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(new OperationBuilder("missing-request-fields", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java new file mode 100644 index 000000000..8dbff585d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2016 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.util.Arrays; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + */ +public class RequestPartsFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartsFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestWithFields() throws IOException { + this.snippet.expectRequestFields("map-request-parts-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three"))) + .document(operationBuilder("map-request-parts-with-fields") + .request("http://localhost") + .part("part", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +} From d2a5b38c83609e83c405e7b99a49e39fa7d8038c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 12:04:43 +0100 Subject: [PATCH 0353/1059] Polish "Add support for documenting request part payload fields" - Rebase on latest code, and make use of new support for the same TemplatedSnippet producing multiple snippets with different names from the same template - Expand the documentation - Apply code formatting - Add support for relaxed documentation of a request part's fields Closes gh-270 --- .../docs/asciidoc/documenting-your-api.adoc | 56 ++++- .../example/mockmvc/RequestPartPayload.java | 48 ++++ .../restassured/RequestPartPayload.java | 48 ++++ .../payload/AbstractFieldsSnippet.java | 31 ++- .../payload/PayloadDocumentation.java | 234 ++++++++++++++---- .../payload/RequestPartFieldsSnippet.java | 87 ++++--- .../default-request-part-fields.snippet | 10 + .../default-request-part-fields.snippet | 5 + ...RequestPartFieldsSnippetFailureTests.java} | 39 +-- .../RequestPartFieldsSnippetTests.java | 118 +++++++++ .../RequestPartsFieldsSnippetTests.java | 56 ----- .../restdocs/test/ExpectedSnippets.java | 4 + 12 files changed, 557 insertions(+), 179 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/RequestPartPayload.java create mode 100644 docs/src/test/java/com/example/restassured/RequestPartPayload.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet rename spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/{RequestPartsFieldsSnippetFailureTests.java => RequestPartFieldsSnippetFailureTests.java} (62%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 96117ffda..4b3d58fac 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -415,7 +415,7 @@ include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request- ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. +<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on @@ -575,12 +575,58 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. -[[documenting-your-api-request-parts]] -=== Request parts content +[[documenting-your-api-request-parts-payloads]] +=== Request part payloads + +The payload of a request part can be documented in much the same way as the +<>: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. + payload. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +The result is a snippet that contains a table describing the part's fields. This snippet +is named `request-part-${part-name}-fields.adoc`. For example, documenting a part named +`metadata` will produce a snippet named `request-part-metadata-fields.adoc`. + +When documenting fields, the test will fail if an undocumented field is found in the +payload of the part. Similarly, the test will also fail if a documented field is not found +in the payload of the part and the field has not been marked as optional. For payloads +with a hierarchical structure, documenting a field is sufficient for all of its +descendants to also be treated as having been documented. -If you need to document the content of request part that holds Json data, you should use`requestPartsFields`. -It acts exactly as [[documenting-your-api-request-parts]] but in order to use it, you must specify the part name. +If you do not want to document a field, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + +Fields can also be documented in a relaxed mode where any undocumented fields will not +cause a test failure. To do so, use the `relaxedRequestPartFields` method on +`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the payload +of the part. +For further information on describing fields, documenting payloads that use XML, +and more please refer to the +<>. [[documenting-your-api-http-headers]] === HTTP headers diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java new file mode 100644 index 000000000..56d6ae85d --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestPartPayload { + + private MockMvc mockMvc; + + public void response() throws Exception { + // tag::payload[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))); // <2> + // end::payload[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java new file mode 100644 index 000000000..5dc8eda54 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestPartPayload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::payload[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))) // <2> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::payload[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 16f7ba61b..ae6fb3fe8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -65,10 +65,11 @@ protected AbstractFieldsSnippet(String type, List descriptors, /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named - * {@code -fields}. The fields will be documented using the given - * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, - * undocumented fields will be ignored and will not trigger a failure. + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors @@ -77,7 +78,27 @@ protected AbstractFieldsSnippet(String type, List descriptors, */ protected AbstractFieldsSnippet(String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(type + "-fields", attributes); + this(type, type, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super(name + "-fields", type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index fed726f1d..2e1d8ea2f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -148,56 +148,6 @@ public static RequestFieldsSnippet requestFields(List descripto return new RequestFieldsSnippet(descriptors); } - /** - * Returns a {@code Snippet} that will document the fields of the specified {@code part} - * of the API operations's request payload. The fields will be documented using the - * given {@code descriptors}. - *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param part the part name - * @param descriptors the descriptions of the request payload's fields - * @return the snippet that will document the fields - * @see #fieldWithPath(String) - */ - public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { - return requestPartFields(part, Arrays.asList(descriptors)); - } - - /** - * Returns a {@code Snippet} that will document the fields of the specified {@code part} - * of the API operations's request payload. The fields will be documented using the given - * {@code descriptors}. - *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param part the part name - * @param descriptors the descriptions of the request payload's fields - * @return the snippet that will document the fields - * @see #fieldWithPath(String) - */ - public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors); - } - /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -318,6 +268,190 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java index 4de8f8486..f07923deb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -29,10 +29,12 @@ import org.springframework.restdocs.snippet.SnippetException; /** - * A {@link Snippet} that documents the fields in a request. + * A {@link Snippet} that documents the fields in a request part. * * @author Mathieu Pousse + * @author Andy Wilkinson * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + * @see PayloadDocumentation#requestPartFields(String, List) */ public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { @@ -40,27 +42,29 @@ public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the - * request part using the given {@code descriptors}. Undocumented fields will trigger a - * failure. + * request part using the given {@code descriptors}. Undocumented fields will trigger + * a failure. * - * @param partName the part name + * @param partName the part name * @param descriptors the descriptors */ - protected RequestPartFieldsSnippet(String partName, List descriptors) { + protected RequestPartFieldsSnippet(String partName, + List descriptors) { this(partName, descriptors, null, false); } /** * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the - * request part using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is - * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * request part using the given {@code descriptors}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * - * @param partName the part name - * @param descriptors the descriptors + * @param partName the part name + * @param descriptors the descriptors * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ protected RequestPartFieldsSnippet(String partName, List descriptors, - boolean ignoreUndocumentedFields) { + boolean ignoreUndocumentedFields) { this(partName, descriptors, null, ignoreUndocumentedFields); } @@ -70,12 +74,12 @@ protected RequestPartFieldsSnippet(String partName, List descri * included in the model during template rendering. Undocumented fields will trigger a * failure. * - * @param partName the part name + * @param partName the part name * @param descriptors the descriptors - * @param attributes the additional attributes + * @param attributes the additional attributes */ protected RequestPartFieldsSnippet(String partName, List descriptors, - Map attributes) { + Map attributes) { this(partName, descriptors, attributes, false); } @@ -86,44 +90,36 @@ protected RequestPartFieldsSnippet(String partName, List descri * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be * ignored and will not trigger a failure. * - * @param partName the part name - * @param descriptors the descriptors - * @param attributes the additional attributes + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ protected RequestPartFieldsSnippet(String partName, List descriptors, - Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + Map attributes, boolean ignoreUndocumentedFields) { + super("request-part-" + partName, "request-part", descriptors, attributes, + ignoreUndocumentedFields); this.partName = partName; } @Override protected MediaType getContentType(Operation operation) { - for (OperationRequestPart candidate : operation.getRequest().getParts()) { - if (candidate.getName().equals(this.partName)) { - return candidate.getHeaders().getContentType(); - } - } - throw new SnippetException(missingPartErrorMessage()); + return findPart(operation).getHeaders().getContentType(); } @Override protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + private OperationRequestPart findPart(Operation operation) { for (OperationRequestPart candidate : operation.getRequest().getParts()) { if (candidate.getName().equals(this.partName)) { - return candidate.getContent(); + return candidate; } } - throw new SnippetException(missingPartErrorMessage()); - } - - /** - * Prepare the error message because the requested part was not found. - * - * @return see description - */ - protected String missingPartErrorMessage() { - return "Request parts with the following names were not found in the request: " + this.partName; + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); } /** @@ -146,7 +142,8 @@ public final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescripto * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final RequestPartFieldsSnippet and(List additionalDescriptors) { + public final RequestPartFieldsSnippet and( + List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -156,17 +153,18 @@ public final RequestPartFieldsSnippet and(List additionalDescri * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. * - * @param pathPrefix the prefix to apply to the additional descriptors + * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, - FieldDescriptor... additionalDescriptors) { + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); - return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); } /** @@ -175,17 +173,18 @@ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. * - * @param pathPrefix the prefix to apply to the additional descriptors + * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, - List additionalDescriptors) { + List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); combinedDescriptors.addAll( PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); - return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet new file mode 100644 index 000000000..46cd43fe4 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet new file mode 100644 index 000000000..27a4e4379 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +`{{path}}` | `{{type}}` | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java similarity index 62% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java index a025514ce..03cf666ec 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java @@ -26,7 +26,6 @@ import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -38,11 +37,13 @@ * undocumented fields. * * @author Mathieu Pousse + * @author Andy Wilkinson */ -public class RequestPartsFieldsSnippetFailureTests { +public class RequestPartFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -50,33 +51,33 @@ public class RequestPartsFieldsSnippetFailureTests { @Test public void undocumentedRequestPartField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); new RequestPartFieldsSnippet("part", Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": 5}".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); } @Test public void missingRequestPartField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": 5}".getBytes()).build()); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestPartFieldsSnippet("part", + Arrays.asList(fieldWithPath("b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); } @Test public void missingRequestPart() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Request parts with the following names were not found in the request: another")); - new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + this.thrown.expectMessage( + equalTo("A request part named 'another' was not found in the request")); + new RequestPartFieldsSnippet("another", + Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java new file mode 100644 index 000000000..0d4c7c32c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014-2016 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.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + */ +public class RequestPartFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestPartFields() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), fieldWithPath("a") + .description("three"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + + @Test + public void multipleRequestParts() throws IOException { + this.snippets.expectRequestPartFields("one"); + this.snippets.expectRequestPartFields("two"); + Operation operation = this.operationBuilder.request("http://localhost") + .part("one", "{}".getBytes()).and().part("two", "{}".getBytes()).build(); + new RequestPartFieldsSnippet("one", Collections.emptyList()) + .document(operation); + new RequestPartFieldsSnippet("two", Collections.emptyList()) + .document(operation); + } + + @Test + public void allUndocumentedRequestPartFieldsCanBeIgnored() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": 5, \"b\": 4}".getBytes()).build()); + } + + @Test + public void additionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java deleted file mode 100644 index 8dbff585d..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014-2016 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.util.Arrays; - -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateFormat; - -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for {@link RequestPartFieldsSnippet}. - * - * @author Mathieu Pousse - */ -public class RequestPartsFieldsSnippetTests extends AbstractSnippetTests { - - public RequestPartsFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-parts-with-fields") - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); - - new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("a.b").description("one"), - fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))) - .document(operationBuilder("map-request-parts-with-fields") - .request("http://localhost") - .part("part", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) - .build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java index f57c5b959..3a174532f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java @@ -81,6 +81,10 @@ public ExpectedSnippet expectRequestFields() { return expect("request-fields"); } + public ExpectedSnippet expectRequestPartFields(String partName) { + return expect("request-part-" + partName + "-fields"); + } + public ExpectedSnippet expectResponseFields() { return expect("response-fields"); } From 60211dab868767711a47dc3f4ad5e19cad213869 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 16:10:49 +0100 Subject: [PATCH 0354/1059] Polishing - Pass attributes into RequestPartFieldsSnippet - Add missing package.info.java for new asciidoctor package --- .../restdocs/asciidoctor/package-info.java | 20 +++++++++++++++++++ .../payload/PayloadDocumentation.java | 8 ++++---- 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java new file mode 100644 index 000000000..45e050e7f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Spring REST Docs Asciidoctor extensions. + */ +package org.springframework.restdocs.asciidoctor; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 2e1d8ea2f..bb6bbbc28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -381,7 +381,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { - return requestPartFields(part, Arrays.asList(descriptors)); + return requestPartFields(part, attributes, Arrays.asList(descriptors)); } /** @@ -409,7 +409,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors); + return new RequestPartFieldsSnippet(part, descriptors, attributes); } /** @@ -429,7 +429,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { - return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + return relaxedRequestPartFields(part, attributes, Arrays.asList(descriptors)); } /** @@ -449,7 +449,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors, true); + return new RequestPartFieldsSnippet(part, descriptors, attributes, true); } /** From 7bcfbd9e35ebcb025ceb67876362934c0289dc16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 10:16:46 +0100 Subject: [PATCH 0355/1059] Add support for documenting a portion of a request or response payload Closes gh-312 --- .../docs/asciidoc/documenting-your-api.adoc | 63 ++ docs/src/test/java/com/example/Payload.java | 10 + .../java/com/example/mockmvc/Payload.java | 23 +- .../java/com/example/restassured/Payload.java | 12 + .../payload/AbstractFieldsSnippet.java | 91 ++- .../FieldPathPayloadSubsectionExtractor.java | 108 +++ .../payload/PayloadDocumentation.java | 675 ++++++++++++++++++ .../payload/PayloadHandlingException.java | 15 +- .../payload/PayloadSubsectionExtractor.java | 55 ++ .../payload/RequestFieldsSnippet.java | 73 +- .../payload/RequestPartFieldsSnippet.java | 76 +- .../payload/ResponseFieldsSnippet.java | 76 +- ...ldPathPayloadSubsectionExtractorTests.java | 110 +++ .../payload/RequestFieldsSnippetTests.java | 28 + .../RequestPartFieldsSnippetTests.java | 20 + .../payload/ResponseFieldsSnippetTests.java | 14 + 16 files changed, 1421 insertions(+), 28 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 4b3d58fac..557a13d26 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -402,6 +402,69 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] <1> Document the array <2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` +[[documenting-your-api-request-response-payloads-subsections]] +==== Documenting a portion of a request or response payload + +If a payload is large or structurally complex, it can be useful to document +individual sections of the payload. REST Docs allows you to do so by extracting a +subsection of the payload and then documenting it. + +Consider the following JSON response payload: + +[source,json,indent=0] +---- + { + "weather": { + "wind": { + "speed": 15.3, + "direction": 287.0 + }, + "temperature": { + "high": 21.2, + "low": 14.8 + } + } + } +---- + +A snippet that documents the fields of the `temperature` object (`high` and `low`) can +be produced as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +The result is a snippet that contains a table describing the `high` and `low` fields +of `weather.temperature`. To make the snippet's name distinct, an identifier for the +subsection is included. By default, this identifier is `beneath-${path}`. For +example, the code above will result in a snippet named +`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using +the `withSubsectionId(String)` method: + +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `response-fields-temp.adoc`. + + + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index d33de4d16..f553927df 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -18,7 +18,9 @@ import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; public class Payload { @@ -31,4 +33,12 @@ public void bookFieldDescriptors() { // end::book-descriptors[] } + public void customSubsectionId() { + // tag::custom-subsection-id[] + responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), + fieldWithPath("high").description("…"), + fieldWithPath("low").description("…")); + // end::custom-subsection-id[] + } + } diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 615354a3f..9701f29f1 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -16,21 +16,22 @@ package com.example.mockmvc; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.web.servlet.MockMvc; - public class Payload { private MockMvc mockMvc; @@ -91,4 +92,14 @@ public void descriptorReuse() throws Exception { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))); + // end::subsection[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 155e357e5..d54a7e6db 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -23,6 +23,7 @@ import com.jayway.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -95,4 +96,15 @@ public void descriptorReuse() throws Exception { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::subsection[] + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index ae6fb3fe8..a630cc385 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -45,6 +45,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final String type; + private final PayloadSubsectionExtractor subsectionExtractor; + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given @@ -81,6 +83,29 @@ protected AbstractFieldsSnippet(String type, List descriptors, this(type, type, descriptors, attributes, ignoreUndocumentedFields); } + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload extracted by the given {@code subsectionExtractor} + * will be documented using the given {@code descriptors} and the given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + this(type, type, descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); + } + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields} using a template named {@code -fields}. The fields will @@ -98,7 +123,35 @@ protected AbstractFieldsSnippet(String type, List descriptors, protected AbstractFieldsSnippet(String name, String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(name + "-fields", type + "-fields", attributes); + this(name, type, descriptors, attributes, ignoreUndocumentedFields, null); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload identified by {@code subsectionPath} will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor documented. {@code null} or an + * empty string can be used to indicate that the entire payload should be documented. + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + super(name + "-fields" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { @@ -112,11 +165,24 @@ protected AbstractFieldsSnippet(String name, String type, this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; this.type = type; + this.subsectionExtractor = subsectionExtractor; } @Override protected Map createModel(Operation operation) { - ContentHandler contentHandler = getContentHandler(operation); + byte[] content; + try { + content = verifyContent(getContent(operation)); + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + MediaType contentType = getContentType(operation); + if (this.subsectionExtractor != null) { + content = verifyContent( + this.subsectionExtractor.extractSubsection(content, contentType)); + } + ContentHandler contentHandler = getContentHandler(content, contentType); validateFieldDocumentation(contentHandler); @@ -146,28 +212,27 @@ protected Map createModel(Operation operation) { return model; } - private ContentHandler getContentHandler(Operation operation) { - MediaType contentType = getContentType(operation); - ContentHandler contentHandler; + private byte[] verifyContent(byte[] content) { + if (content.length == 0) { + throw new SnippetException("Cannot document " + this.type + " fields as the " + + this.type + " body is empty"); + } + return content; + } + private ContentHandler getContentHandler(byte[] content, MediaType contentType) { try { - byte[] content = getContent(operation); - if (content.length == 0) { - throw new SnippetException("Cannot document " + this.type - + " fields as the " + this.type + " body is empty"); - } if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(content); + return new XmlContentHandler(content); } else { - contentHandler = new JsonContentHandler(content); + return new JsonContentHandler(content); } } catch (IOException ex) { throw new ModelCreationException(ex); } - return contentHandler; } private void validateFieldDocumentation(ContentHandler payloadHandler) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java new file mode 100644 index 000000000..ad948e820 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.http.MediaType; + +/** + * A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload + * identified by a field path. + * + * @author Andy Wilkinson + * @since 1.2.0 + * @see PayloadDocumentation#beneathPath(String) + */ +public class FieldPathPayloadSubsectionExtractor + implements PayloadSubsectionExtractor { + + private final String fieldPath; + + private final String subsectionId; + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath}. The + * {@code fieldPath} prefixed with {@code beneath-} with be used as the subsection ID. + * + * @param fieldPath the path of the field + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath) { + this(fieldPath, "beneath-" + fieldPath); + } + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath} and that will + * us the given {@code subsectionId} to identify the subsection. + * + * @param fieldPath the path of the field + * @param subsectionId the ID of the subsection + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectionId) { + this.fieldPath = fieldPath; + this.subsectionId = subsectionId; + } + + @Override + public byte[] extractSubsection(byte[] payload, MediaType contentType) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonFieldPath compiledPath = JsonFieldPath.compile(this.fieldPath); + Object extracted = new JsonFieldProcessor().extract(compiledPath, + objectMapper.readValue(payload, Object.class)); + if (extracted instanceof List && !compiledPath.isPrecise()) { + List extractedList = (List) extracted; + if (extractedList.size() == 1) { + extracted = extractedList.get(0); + } + else { + throw new PayloadHandlingException(this.fieldPath + + " does not uniquely identify a subsection of the payload"); + } + } + return objectMapper.writeValueAsBytes(extracted); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + @Override + public String getSubsectionId() { + return this.subsectionId; + } + + /** + * Returns the path of the field that will be extracted. + * + * @return the path of the field + */ + protected String getFieldPath() { + return this.fieldPath; + } + + @Override + public FieldPathPayloadSubsectionExtractor withSubsectionId(String subsectionId) { + return new FieldPathPayloadSubsectionExtractor(this.fieldPath, subsectionId); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bb6bbbc28..286cd185a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -268,6 +268,216 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the subsection of API + * operations's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors}. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields in the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field is sufficient for all of its descendants to also be treated as having been + * documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

            + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Returns a {@code Snippet} that will document the fields of the specified * {@code part} of the API operations's request payload. The fields will be documented @@ -287,6 +497,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -452,6 +663,235 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, return new RequestPartFieldsSnippet(part, descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the givne {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

            + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, + attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, + attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -494,7 +934,9 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( List descriptors) { @@ -511,7 +953,9 @@ public static ResponseFieldsSnippet responseFields( * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( FieldDescriptor... descriptors) { @@ -623,6 +1067,225 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

            + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field is sufficient for all of its descendants to also be treated as + * having been documented. + *

            + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

            + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. @@ -651,6 +1314,18 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } + /** + * Returns a {@link PayloadSubsectionExtractor} that will extract the subsection of + * the JSON payload found beneath the given {@code path}. + * + * @param path the path + * @return the subsection extractor + * @since 1.2.0 + */ + public static PayloadSubsectionExtractor beneathPath(String path) { + return new FieldPathPayloadSubsectionExtractor(path); + } + private static Attribute[] asArray(Map attributeMap) { List attributes = new ArrayList<>(); for (Map.Entry attribute : attributeMap.entrySet()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index 383ba2221..6619f5ae5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -26,7 +26,18 @@ class PayloadHandlingException extends RuntimeException { /** - * Creates a new {@code PayloadHandlingException} with the given cause. + * Creates a new {@code PayloadHandlingException} with the given {@code message}. + * + * @param message the message + * @since 1.2.0 + */ + PayloadHandlingException(String message) { + super(message); + } + + /** + * Creates a new {@code PayloadHandlingException} with the given {@code cause}. + * * @param cause the cause of the failure */ PayloadHandlingException(Throwable cause) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java new file mode 100644 index 000000000..c69989baa --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2016 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 org.springframework.http.MediaType; + +/** + * Strategy interface for extracting a subsection of a payload. + * + * @param The subsection extractor subclass + * @author Andy Wilkinson + * @since 1.2.0 + */ +public interface PayloadSubsectionExtractor> { + + /** + * Extracts a subsection of the given {@code payload} that has the given + * {@code contentType}. + * + * @param payload the payload + * @param contentType the content type of the payload + * @return the subsection of the payload + */ + byte[] extractSubsection(byte[] payload, MediaType contentType); + + /** + * Returns an identifier for the subsection that this extractor will extract. + * + * @return the identifier + */ + String getSubsectionId(); + + /** + * Returns an extractor with the given {@code subsectionId}. + * + * @param subsectionId the subsection ID + * @return the customized extractor + */ + T withSubsectionId(String subsectionId); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index ea8f68b2c..9be9e32c5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -86,7 +86,74 @@ protected RequestFieldsSnippet(List descriptors, */ protected RequestFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor document + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param subsectionExtractor the path identifying the subsection of the payload to + * document + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -137,8 +204,8 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java index f07923deb..1eb5205db 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -33,6 +33,7 @@ * * @author Mathieu Pousse * @author Andy Wilkinson + * @since 1.2.0 * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) * @see PayloadDocumentation#requestPartFields(String, List) */ @@ -97,8 +98,81 @@ protected RequestPartFieldsSnippet(String partName, List descri */ protected RequestPartFieldsSnippet(String partName, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { + this(partName, null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. Undocumented fields + * will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(partName, subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(partName, subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(partName, subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { super("request-part-" + partName, "request-part", descriptors, attributes, - ignoreUndocumentedFields); + ignoreUndocumentedFields, subsectionExtractor); this.partName = partName; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index cabe29484..79d306ea8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -87,7 +87,77 @@ protected ResponseFieldsSnippet(List descriptors, */ protected ResponseFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("response", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. Undocumented fields will + * trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -138,8 +208,8 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java new file mode 100644 index 000000000..eabc887a4 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2016 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.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.MediaType; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link FieldPathPayloadSubsectionExtractor}. + * + * @author Andy Wilkinson + */ +public class FieldPathPayloadSubsectionExtractorTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.b") + .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMultiElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a") + .extractSubsection("{\"a\":[{\"b\":5},{\"b\":4}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(2))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + assertThat(extracted.get(1).get("b"), is(equalTo((Object) 4))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractSingleElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[]") + .extractSubsection("{\"a\":[{\"b\":5}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionFromSingleElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") + .extractSubsection("{\"a\":[{\"b\":{\"c\":5}}]}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + public void extractMapSubsectionFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + this.thrown.expect(PayloadHandlingException.class); + this.thrown.expectMessage( + equalTo("a.[].b does not uniquely identify a subsection of the payload")); + new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6}}]}".getBytes(), + MediaType.APPLICATION_JSON); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 8e6d8414d..d3ad4e25c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -33,7 +33,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -63,6 +65,19 @@ public void mapRequestWithFields() throws IOException { .build()); } + @Test + public void subsectionOfMapRequest() throws IOException { + this.snippets.expect("request-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayRequestWithFields() throws IOException { this.snippets.expectRequestFields() @@ -81,6 +96,19 @@ public void arrayRequestWithFields() throws IOException { .build()); } + @Test + public void subsectionOfArrayRequest() throws IOException { + this.snippets.expect("request-fields-beneath-[].a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("[].a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": 5, \"c\": \"charlie\"}}]") + .build()); + } + @Test public void ignoredRequestField() throws IOException { this.snippets.expectRequestFields() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java index 0d4c7c32c..8a1334eea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.templates.TemplateFormat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; /** @@ -60,6 +61,25 @@ public void mapRequestPartFields() throws IOException { .build()); } + @Test + public void mapRequestPartSubsectionFields() throws IOException { + this.snippets.expect("request-part-one-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + new RequestPartFieldsSnippet("one", + beneathPath("a"), Arrays + .asList(fieldWithPath("b").description("one"), + fieldWithPath("c").description("two"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + @Test public void multipleRequestParts() throws IOException { this.snippets.expectRequestPartFields("one"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 59002896a..3b8c17287 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -33,7 +33,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -70,6 +72,18 @@ public void mapResponseWithFields() throws IOException { .build()); } + @Test + public void subsectionOfMapResponse() throws IOException { + this.snippets.expect("response-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + responseFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayResponseWithFields() throws IOException { this.snippets.expectResponseFields() From cbd96f301d87078b01c6852723bf0861bdfbca11 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 17:33:34 +0100 Subject: [PATCH 0356/1059] Update field snippets to no longer document whole subsection by default Previously, when a field was documented it would implicitly document the whole subsection of the payload identified by that field. This could lead to users inadvertently failing to document part of the payload. Arguably, this was a bug as it violated REST Docs' principle of producing accurate, detail documentation. However, fixing it requires a breaking change as people may also be relying on this behaviour. A balance needed to be struck so the fix is being made in a minor release. This commit introduces a new subsectionWithPath method which returns a SubsectionDescriptor; a specialisation of FieldDescriptor. Users that were intentionally relying on the old behaviour will have to replace some usage of fieldWithPath with subsectionWithPath instead. Users who were unintentionally relying on the old behaviour will have to add some additional descriptors produced using fieldWithPath and will receive more accurate documentation in return. Closes gh-274 --- .../docs/asciidoc/documenting-your-api.adoc | 67 ++- .../java/com/example/mockmvc/Payload.java | 20 +- .../java/com/example/restassured/Payload.java | 19 +- .../com/example/ApiDocumentationSpec.groovy | 9 +- .../com/example/notes/ApiDocumentation.java | 19 +- .../com/example/notes/ApiDocumentation.java | 33 +- .../payload/AbstractFieldsSnippet.java | 8 +- .../restdocs/payload/FieldDescriptor.java | 2 +- .../restdocs/payload/JsonContentHandler.java | 11 +- .../restdocs/payload/JsonFieldProcessor.java | 65 ++ .../payload/PayloadDocumentation.java | 553 +++++++++++------- .../payload/SubsectionDescriptor.java | 37 ++ .../restdocs/payload/XmlContentHandler.java | 38 +- .../payload/JsonFieldProcessorTests.java | 65 ++ .../RequestFieldsSnippetFailureTests.java | 18 +- .../payload/RequestFieldsSnippetTests.java | 40 ++ .../payload/XmlContentHandlerTests.java | 91 +++ ...kMvcRestDocumentationIntegrationTests.java | 8 +- ...uredRestDocumentationIntegrationTests.java | 3 +- 19 files changed, 842 insertions(+), 264 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 557a13d26..a1eeb908f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -110,7 +110,19 @@ include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. For example: +provided. Consider the following payload: + +[source,json,indent=0] +---- + { + "contact": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + } + } +---- + +It can be documented like this: [source,java,indent=0,role="primary"] .MockMvc @@ -120,9 +132,9 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response] <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method + on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. [source,java,indent=0,role="secondary"] .REST Assured @@ -132,9 +144,9 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=response] <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on +<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<3> Expect a field with the path `contact.name`. The result is a snippet that contains a table describing the fields. For requests this snippet is named `request-fields.adoc`. For responses this snippet is named @@ -142,12 +154,36 @@ snippet is named `request-fields.adoc`. For responses this snippet is named When documenting fields, the test will fail if an undocumented field is found in the payload. Similarly, the test will also fail if a documented field is not found in the -payload and the field has not been marked as optional. For payloads with a hierarchical -structure, documenting a field is sufficient for all of its descendants to also be -treated as having been documented. +payload and the field has not been marked as optional. -If you do not want to document a field, you can mark it as ignored. This will prevent it -from appearing in the generated snippet while avoiding the failure described above. +If you don't want to provide detailed documentation for all of the fields, an entire +subsection of a payload can be documented. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` + are now seen has having also been documented. Uses the static `subsectionWithPath` + method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` + are now seen has having also been documented. Uses the static `subsectionWithPath` + method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +`subsectionWithPath` can be useful for providing a high-level overview of a particular +section of a payload. Separate, more detailed documentation for a subsection can then +<>. + +If you do not want to document a field or subsection at all, you can mark it as ignored. +This will prevent it from appearing in the generated snippet while avoiding the failure +described above. Fields can also be documented in a relaxed mode where any undocumented fields will not cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields` @@ -233,7 +269,7 @@ The following paths are all present: |=== -A response that uses an array at its root can also be documented. The path `[]` will refer +A payload that uses an array at its root can also be documented. The path `[]` will refer to the entire array. You can then use bracket or dot notation to identify fields within the array's entries. For example, `[].id` corresponds to the `id` field of every object found in the following array: @@ -403,7 +439,7 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] <2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` [[documenting-your-api-request-response-payloads-subsections]] -==== Documenting a portion of a request or response payload +==== Documenting a subsection of a request or response payload If a payload is large or structurally complex, it can be useful to document individual sections of the payload. REST Docs allows you to do so by extracting a @@ -433,7 +469,7 @@ be produced as follows: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -443,7 +479,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -457,6 +493,7 @@ example, the code above will result in a snippet named `response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using the `withSubsectionId(String)` method: +[source,java,indent=0] ---- include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] ---- diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 9701f29f1..5802e2602 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -26,6 +26,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -41,10 +42,19 @@ public void response() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(document("index", responseFields( // <1> - fieldWithPath("contact").description("The user's contact details"), // <2> - fieldWithPath("contact.email").description("The user's email address")))); // <3> + fieldWithPath("contact.email").description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } + + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("index", responseFields( // <1> + subsectionWithPath("contact").description("The user's contact details")))); // <1> + // end::subsection[] + } public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) @@ -92,14 +102,14 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsection() throws Exception { - // tag::subsection[] + public void subsectionBeneathPath() throws Exception { + // tag::beneath-path[] this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))); - // end::subsection[] + // end::beneath-path[] } } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index d54a7e6db..c5734d429 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -27,6 +27,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -39,12 +40,22 @@ public void response() throws Exception { // tag::response[] RestAssured.given(this.spec).accept("application/json") .filter(document("user", responseFields( // <1> - fieldWithPath("contact").description("The user's contact details"), // <2> + fieldWithPath("contact.name").description("The user's name"), // <2> fieldWithPath("contact.email").description("The user's email address")))) // <3> .when().get("/user/5") .then().assertThat().statusCode(is(200)); // end::response[] } + + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("user", responseFields( + subsectionWithPath("contact").description("The user's contact details")))) // <1> + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + // end::response[] + } public void explicitType() throws Exception { RestAssured.given(this.spec).accept("application/json") @@ -96,15 +107,15 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsection() throws Exception { - // tag::subsection[] + public void subsectionBeneathPath() throws Exception { + // tag::beneath-path[] RestAssured.given(this.spec).accept("application/json") .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))) .when().get("/locations/1") .then().assertThat().statusCode(is(200)); - // end::subsection[] + // end::beneath-path[] } } diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 2c014fc01..102426b96 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -26,6 +26,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration @@ -75,8 +76,8 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('appprofile').description('the profile of grails used in this project'), fieldWithPath('groovyversion').description('the version of groovy used in this project'), fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + subsectionWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + subsectionWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), ))) .when() .port(this.serverPort) @@ -123,14 +124,14 @@ class ApiDocumentationSpec extends Specification { requestFields( fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') ), responseFields( fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note') ))) .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') .when() 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 82a30e34b..bfeeb6269 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -28,6 +28,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -59,7 +60,7 @@ @SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) @WebAppConfiguration public class ApiDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @@ -116,7 +117,7 @@ public void indexExample() throws Exception { linkWithRel("tags").description("The <>"), linkWithRel("profile").description("The ALPS profile for the service")), responseFields( - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @@ -137,8 +138,8 @@ public void notesListExample() throws Exception { linkWithRel("self").description("Canonical link for this resource"), linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.notes").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_embedded.notes").description("An array of <>"), + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -208,7 +209,7 @@ public void noteGetExample() throws Exception { responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -227,8 +228,8 @@ public void tagsListExample() throws Exception { linkWithRel("self").description("Canonical link for this resource"), linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.tags").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_embedded.tags").description("An array of <>"), + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -310,7 +311,7 @@ public void tagGetExample() throws Exception { linkWithRel("notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test 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 f63458406..55b1e0368 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 @@ -33,6 +33,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -68,11 +69,11 @@ @SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) @WebAppConfiguration public class ApiDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - private RestDocumentationResultHandler documentationHandler; + + private RestDocumentationResultHandler documentationHandler; @Autowired private NoteRepository noteRepository; @@ -93,13 +94,13 @@ public void setUp() { this.documentationHandler = document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())); - + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(this.documentationHandler) .build(); } - + @Test public void headersExample() throws Exception { this.mockMvc @@ -140,7 +141,7 @@ public void indexExample() throws Exception { linkWithRel("notes").description("The <>"), linkWithRel("tags").description("The <>")), responseFields( - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -150,13 +151,13 @@ public void notesListExample() throws Exception { createNote("REST maturity model", "http://martinfowler.com/articles/richardsonMaturityModel.html"); createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - + this.mockMvc .perform(get("/notes")) .andExpect(status().isOk()) .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + subsectionWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -177,7 +178,7 @@ public void notesCreateExample() throws Exception { note.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NoteInput.class); - + this.mockMvc .perform(post("/notes") .contentType(MediaTypes.HAL_JSON) @@ -214,7 +215,7 @@ public void noteGetExample() throws Exception { .content(this.objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andReturn().getResponse().getHeader("Location"); - + this.mockMvc .perform(get(noteLocation)) .andExpect(status().isOk()) @@ -229,7 +230,7 @@ public void noteGetExample() throws Exception { responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @@ -241,13 +242,13 @@ public void tagsListExample() throws Exception { createTag("REST"); createTag("Hypermedia"); createTag("HTTP"); - + this.mockMvc .perform(get("/tags")) .andExpect(status().isOk()) .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + subsectionWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -256,7 +257,7 @@ public void tagsCreateExample() throws Exception { tag.put("name", "REST"); ConstrainedFields fields = new ConstrainedFields(TagInput.class); - + this.mockMvc .perform(post("/tags") .contentType(MediaTypes.HAL_JSON) @@ -344,7 +345,7 @@ public void tagGetExample() throws Exception { linkWithRel("tagged-notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -363,7 +364,7 @@ public void tagUpdateExample() throws Exception { tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); - + this.mockMvc .perform(patch(tagLocation) .contentType(MediaTypes.HAL_JSON) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index a630cc385..8c83ab973 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -155,12 +155,10 @@ protected AbstractFieldsSnippet(String name, String type, for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for field '" + descriptor.getPath() - + "' must either have a description or" + " be marked as " - + "ignored"); + Assert.notNull(descriptor.getDescription() != null, + "The descriptor for '" + descriptor.getPath() + "' must have a" + + " description or it must be marked as ignored"); } - } this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index d68bb68d7..edc484433 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index f1fff9ce4..1dc23a5ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -65,7 +65,12 @@ public String getUndocumentedContent(List fieldDescriptors) { Object content = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, content); + if (describesSubsection(fieldDescriptor)) { + this.fieldProcessor.removeSubsection(path, content); + } + else { + this.fieldProcessor.remove(path, content); + } } if (!isEmpty(content)) { try { @@ -78,6 +83,10 @@ public String getUndocumentedContent(List fieldDescriptors) { return null; } + private boolean describesSubsection(FieldDescriptor fieldDescriptor) { + return fieldDescriptor instanceof SubsectionDescriptor; + } + private Object readContent() { try { return new ObjectMapper().readValue(this.rawContent, Object.class); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index dbaf6d88c..dfbd5ff4a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -76,6 +76,17 @@ public void foundMatch(Match match) { }); } + void removeSubsection(final JsonFieldPath path, Object payload) { + traverse(new ProcessingContext(payload, path), new MatchCallback() { + + @Override + public void foundMatch(Match match) { + match.removeSubsection(); + } + + }); + } + private void traverse(ProcessingContext context, MatchCallback matchCallback) { final String segment = context.getSegment(); if (JsonFieldPath.isArraySegment(segment)) { @@ -149,12 +160,41 @@ public Object getValue() { @Override public void remove() { + Object removalCandidate = this.map.get(this.segment); + if (isMapWithEntries(removalCandidate) + || isListWithNonScalarEntries(removalCandidate)) { + return; + } this.map.remove(this.segment); if (this.map.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.map.remove(this.segment); + if (this.map.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isListWithNonScalarEntries(Object object) { + if (!(object instanceof List)) { + return false; + } + for (Object entry : (List) object) { + if (entry instanceof Map || entry instanceof List) { + return true; + } + } + return false; + } + } private static final class ListMatch implements Match { @@ -181,12 +221,35 @@ public Object getValue() { @Override public void remove() { + if (!itemIsEmpty()) { + return; + } this.items.remove(); if (this.list.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.items.remove(); + if (this.list.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean itemIsEmpty() { + return !isMapWithEntries(this.item) && !isListWithEntries(this.item); + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isListWithEntries(Object object) { + return object instanceof List && !((List) object).isEmpty(); + } + } private interface MatchCallback { @@ -200,6 +263,8 @@ private interface Match { Object getValue(); void remove(); + + void removeSubsection(); } private static final class ProcessingContext { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 286cd185a..3f31ef76e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -102,6 +102,74 @@ public static FieldDescriptor fieldWithPath(String path) { return new FieldDescriptor(path); } + /** + * Creates a {@code FieldDescriptor} that describes a subsection, i.e. a field and all + * of its descendants, with the given {@code path}. + *

            + * When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to + * descend to a child node. + *

            + * When documenting a JSON payload, the {@code path} uses '.' to descend into a child + * object and ' {@code []}' to descend into an array. For example, with this JSON + * payload: + * + *

            +	 * {
            +	 *    "a":{
            +	 *        "b":[
            +	 *            {
            +	 *                "c":"one"
            +	 *            },
            +	 *            {
            +	 *                "c":"two"
            +	 *            },
            +	 *            {
            +	 *                "d":"three"
            +	 *            }
            +	 *        ]
            +	 *    }
            +	 * }
            +	 * 
            + * + * The following paths are all present: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
            PathValue
            {@code a}An object containing "b"
            {@code a.b}An array containing three objects
            {@code a.b[]}An array containing three objects
            {@code a.b[].c}An array containing the strings "one" and "two"
            {@code a.b[].d}The string "three"
            + *

            + * A subsection descriptor for the array with the path {@code a.b[]} will also + * describe its descendants {@code a.b[].c} and {@code a.b[].d}. + * + * @param path The path of the subsection + * @return a {@code SubsectionDescriptor} ready for further configuration + */ + public static SubsectionDescriptor subsectionWithPath(String path) { + return new SubsectionDescriptor(path); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -110,16 +178,19 @@ public static FieldDescriptor fieldWithPath(String path) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the request, * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see FieldDescriptor#description(Object) */ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { return requestFields(Arrays.asList(descriptors)); @@ -133,16 +204,18 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the request, * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(List descriptors) { return new RequestFieldsSnippet(descriptors); @@ -158,6 +231,7 @@ public static RequestFieldsSnippet requestFields(List descripto * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( FieldDescriptor... descriptors) { @@ -174,6 +248,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( List descriptors) { @@ -187,19 +262,21 @@ public static RequestFieldsSnippet relaxedRequestFields( *

            * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { @@ -213,19 +290,21 @@ public static RequestFieldsSnippet requestFields(Map attributes, *

            * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(Map attributes, List descriptors) { @@ -244,6 +323,7 @@ public static RequestFieldsSnippet requestFields(Map attributes, * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, FieldDescriptor... descriptors) { @@ -262,6 +342,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, List descriptors) { @@ -273,22 +354,25 @@ public static RequestFieldsSnippet relaxedRequestFields( * operations's request payload extracted by the given {@code subsectionExtractor}. * The fields will be documented using the given {@code descriptors}. *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur.For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -302,22 +386,25 @@ public static RequestFieldsSnippet requestFields( * API operations's request payload extracted by the given {@code subsectionExtractor} * . The fields will be documented using the given {@code descriptors}. *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request, - * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -339,6 +426,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -360,6 +448,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -374,16 +463,18 @@ public static RequestFieldsSnippet relaxedRequestFields( * The fields will be documented using the given {@code descriptors} and the given * {@code attributes} will be available during snippet generation. *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -391,6 +482,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -405,16 +497,18 @@ public static RequestFieldsSnippet requestFields( * The fields will be documented using the given {@code descriptors} and the given * {@code attributes} will be available during snippet generation. *

            - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -422,6 +516,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -445,6 +540,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -469,6 +565,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -483,22 +580,25 @@ public static RequestFieldsSnippet relaxedRequestFields( * {@code part} of the API operations's request payload. The fields will be documented * using the given {@code descriptors}. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { @@ -510,21 +610,24 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * {@code part} of the API operations's request payload. The fields will be documented * using the given {@code descriptors}. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { @@ -543,6 +646,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, FieldDescriptor... descriptors) { @@ -561,6 +665,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, List descriptors) { @@ -573,22 +678,25 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * using the given {@code descriptors} and the given {@code attributes} will be * available during snippet generation. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param attributes the attributes * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { @@ -601,22 +709,25 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * using the given {@code descriptors} and the given {@code attributes} will be * available during snippet generation. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param attributes the attributes * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, List descriptors) { @@ -637,6 +748,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { @@ -657,6 +769,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, List descriptors) { @@ -669,16 +782,18 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * be extracted by the given {@code subsectionExtractor}. The fields will be * documented using the given {@code descriptors}. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -686,6 +801,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -700,16 +816,18 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * be extracted by the given {@code subsectionExtractor}. The fields will be * documented using the given {@code descriptors}. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -717,6 +835,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -740,6 +859,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -764,6 +884,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -779,16 +900,18 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * documented using the given {@code descriptors} and the given {@code attributes} * will be available during snippet generation. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -797,6 +920,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -813,16 +937,18 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * documented using the given {@code descriptors} and the given {@code attributes} * will be available during snippet generation. *

            - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. - *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

            + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -831,6 +957,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -857,6 +984,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -883,6 +1011,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -899,18 +1028,20 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, *

            * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { return responseFields(Arrays.asList(descriptors)); @@ -923,19 +1054,21 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor *

            * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -955,6 +1088,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -973,6 +1107,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( List descriptors) { @@ -986,19 +1121,21 @@ public static ResponseFieldsSnippet relaxedResponseFields( *

            * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { @@ -1012,19 +1149,21 @@ public static ResponseFieldsSnippet responseFields(Map attribute *

            * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(Map attributes, List descriptors) { @@ -1043,6 +1182,7 @@ public static ResponseFieldsSnippet responseFields(Map attribute * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, FieldDescriptor... descriptors) { @@ -1061,6 +1201,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, List descriptors) { @@ -1077,18 +1218,21 @@ public static ResponseFieldsSnippet relaxedResponseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1107,18 +1251,21 @@ public static ResponseFieldsSnippet responseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1141,6 +1288,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1163,6 +1311,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1182,12 +1331,14 @@ public static ResponseFieldsSnippet relaxedResponseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -1195,6 +1346,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1215,12 +1367,14 @@ public static ResponseFieldsSnippet responseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

            - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -1228,6 +1382,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1252,6 +1407,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1277,6 +1433,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1298,11 +1455,13 @@ public static List applyPathPrefix(String pathPrefix, List descriptors) { List prefixedDescriptors = new ArrayList<>(); for (FieldDescriptor descriptor : descriptors) { - FieldDescriptor prefixedDescriptor = new FieldDescriptor( - pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()) - .attributes(asArray(descriptor.getAttributes())); + String prefixedPath = pathPrefix + descriptor.getPath(); + FieldDescriptor prefixedDescriptor = descriptor instanceof SubsectionDescriptor + ? new SubsectionDescriptor(prefixedPath) + : new FieldDescriptor(prefixedPath); + prefixedDescriptor.description(descriptor.getDescription()) + .type(descriptor.getType()) + .attributes(asArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java new file mode 100644 index 000000000..9d7756bfa --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 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; + +/** + * A description of a subsection, i.e. a field and all of its descendants, in a request or + * response payload. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class SubsectionDescriptor extends FieldDescriptor { + + /** + * Creates a new {@code SubsectionDescriptor} describing the subsection with the given + * {@code path}. + * @param path the path + */ + protected SubsectionDescriptor(String path) { + super(path); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 2e8f99e74..f5a466ff1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import javax.xml.parsers.DocumentBuilder; @@ -36,6 +37,7 @@ import org.w3c.dom.Attr; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -108,6 +110,7 @@ private XPathExpression createXPath(String fieldPath) @Override public String getUndocumentedContent(List fieldDescriptors) { Document payload = readPayload(); + List matchedButNotRemoved = new ArrayList<>(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { NodeList matchingNodes; try { @@ -124,17 +127,50 @@ public String getUndocumentedContent(List fieldDescriptors) { attr.getOwnerElement().removeAttributeNode(attr); } else { - node.getParentNode().removeChild(node); + if (fieldDescriptor instanceof SubsectionDescriptor + || isLeafNode(node)) { + node.getParentNode().removeChild(node); + } + else { + matchedButNotRemoved.add(node); + } } } } + removeLeafNodes(matchedButNotRemoved); if (payload.getChildNodes().getLength() > 0) { return prettyPrint(payload); } return null; } + private void removeLeafNodes(List candidates) { + boolean changed = true; + while (changed) { + changed = false; + Iterator iterator = candidates.iterator(); + while (iterator.hasNext()) { + Node node = iterator.next(); + if (isLeafNode(node)) { + node.getParentNode().removeChild(node); + iterator.remove(); + changed = true; + } + } + } + } + + private boolean isLeafNode(Node node) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (childNodes.item(i) instanceof Element) { + return false; + } + } + return true; + } + private String prettyPrint(Document document) { try { StringWriter stringWriter = new StringWriter(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 395ca4dfb..145f61219 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -201,6 +201,26 @@ public void removeTopLevelMapEntry() { assertThat(payload.size(), equalTo(0)); } + @Test + public void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @Test + public void removeSubsectionRemovesMapWithEntries() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(0)); + } + @Test public void removeNestedMapEntry() { Map payload = new HashMap<>(); @@ -229,6 +249,51 @@ public void removeItemsInNestedArray() throws IOException { assertThat(payload.size(), equalTo(0)); } + @SuppressWarnings("unchecked") + @Test + public void removeDoesNotRemoveArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeDoesNotRemoveArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", + Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeRemovesArrayWithOnlyScalarEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [\"bravo\", \"charlie\"]}", Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeSubsectionRemovesArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeSubsectionRemovesArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", + Map.class); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(0)); + } + @Test public void extractNestedEntryWithDotInKeys() throws IOException { Map payload = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index d6fd39986..80c9cdf14 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -131,8 +131,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); new RequestFieldsSnippet(Collections.emptyList()) .document(this.operationBuilder.request("http://localhost") .content("5").header(HttpHeaders.CONTENT_TYPE, @@ -140,6 +140,20 @@ public void undocumentedXmlRequestField() throws IOException { .build()); } + @Test + public void xmlDescendentsAreNotDocumentedByFieldDescriptor() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a").type("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index d3ad4e25c..9ad807f74 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -36,6 +36,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -65,6 +66,19 @@ public void mapRequestWithFields() throws IOException { .build()); } + @Test + public void entireSubsectionsCanBeDocumented() throws IOException { + this.snippets.expectRequestFields() + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Object`", "one")); + + new RequestFieldsSnippet( + Arrays.asList(subsectionWithPath("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void subsectionOfMapRequest() throws IOException { this.snippets.expect("request-fields-beneath-a") @@ -121,6 +135,18 @@ public void ignoredRequestField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void entireSubsectionCanBeIgnored() throws IOException { + this.snippets.expectRequestFields() + .withContents(tableWithHeader("Path", "Type", "Description").row("`c`", + "`Number`", "Field c")); + + new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").ignored(), + fieldWithPath("c").description("Field c"))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5}, \"c\": 4}").build()); + } + @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { this.snippets.expectRequestFields() @@ -256,6 +282,20 @@ public void xmlRequestFields() throws IOException { .build()); } + @Test + public void entireSubsectionOfXmlPayloadCanBeDocumented() throws IOException { + this.snippets.expectRequestFields().withContents( + tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + + new RequestFieldsSnippet( + Arrays.asList(subsectionWithPath("a").description("one").type("a"))) + .document(this.operationBuilder.request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void additionalDescriptors() throws IOException { this.snippets.expectRequestFields() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java new file mode 100644 index 000000000..5c9f1b6ef --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2016 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.Arrays; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; + +/** + * Tests for {@link XmlContentHandler}. + * + * @author Andy Wilkinson + */ +public class XmlContentHandlerTests { + + @Test + public void topLevelElementCanBeDocumented() { + String undocumentedContent = createHandler("5").getUndocumentedContent( + Arrays.asList(fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void nestedElementCanBeDocumentedLeavingAncestors() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a/b").type("b").description("description"))); + assertThat(undocumentedContent, is(equalTo(String.format("%n")))); + } + + @Test + public void fieldDescriptorDoesNotDocumentEntireSubsection() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays + .asList(fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, + is(equalTo(String.format("%n 5%n%n")))); + } + + @Test + public void subsectionDescriptorDocumentsEntireSubsection() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + subsectionWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void multipleElementsCanBeInDescendingOrderDocumented() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a").type("a").description("description"), + fieldWithPath("a/b").type("b").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void multipleElementsCanBeInAscendingOrderDocumented() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a/b").type("b").description("description"), + fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + private XmlContentHandler createHandler(String xml) { + return new XmlContentHandler(xml.getBytes()); + } + +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5429591fc..26ea2c958 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -72,6 +72,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -316,9 +317,10 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", responseFields( - fieldWithPath("a").description("The description"), - fieldWithPath("links").description("Links to other resources")))); + .andDo(document("links", + responseFields(fieldWithPath("a").description("The description"), + subsectionWithPath("links") + .description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index d1089ecc7..6d1a84e37 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -65,6 +65,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -204,7 +205,7 @@ public void responseFieldsSnippet() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) .filter(document("response-fields", responseFields(fieldWithPath("a").description("The description"), - fieldWithPath("links") + subsectionWithPath("links") .description("Links to other resources")))) .accept("application/json").get("/").then().statusCode(200); From 7ff45d273ec5b92a0c280d08db01637edb37cb3d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 28 Oct 2016 16:19:19 +0100 Subject: [PATCH 0357/1059] Call the right endpoint in REST Assured field reuse example Closes gh-324 --- docs/src/test/java/com/example/restassured/Payload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 155e357e5..75022a487 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -90,7 +90,7 @@ public void descriptorReuse() throws Exception { .filter(document("books", responseFields( fieldWithPath("[]").description("An array of books")) // <1> .andWithPrefix("[].", book))) // <2> - .when().get("/books/1") + .when().get("/books") .then().assertThat().statusCode(is(200)); // end::book-array[] } From 1393182182cd270cffe153be2bf9e69a4b917d44 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 28 Oct 2016 21:25:30 +0100 Subject: [PATCH 0358/1059] Upgrade Spring Boot-based samples and integration tests to use 1.4 Closes gh-327 --- samples/rest-assured/build.gradle | 5 ++--- .../SampleRestAssuredApplicationTests.java | 17 ++++++++--------- samples/rest-notes-slate/build.gradle | 2 +- .../com/example/notes/ApiDocumentation.java | 13 ++++++------- samples/rest-notes-spring-data-rest/pom.xml | 3 +-- .../com/example/notes/ApiDocumentation.java | 10 ++++------ .../notes/GettingStartedDocumentation.java | 14 ++++++-------- .../rest-notes-spring-hateoas/build.gradle | 3 +-- .../com/example/notes/ApiDocumentation.java | 10 ++++------ .../notes/GettingStartedDocumentation.java | 14 ++++++-------- samples/testng/build.gradle | 7 +++---- .../testng/SampleTestNgApplicationTests.java | 6 ++---- spring-restdocs-restassured/build.gradle | 19 +++---------------- .../RestAssuredRequestConverterTests.java | 18 +++++++----------- ...uredRestDocumentationIntegrationTests.java | 14 +++++--------- 15 files changed, 59 insertions(+), 96 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index b8776d376..a55dd6b13 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -31,12 +31,11 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' -ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" + testCompile 'org.springframework.restdocs:spring-restdocs-restassured' asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" } diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java index 6eea9876d..c1c4de794 100644 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -27,26 +27,25 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; -@SpringApplicationConfiguration(classes=SampleRestAssuredApplication.class) -@WebIntegrationTest("server.port=0") +@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) @RunWith(SpringJUnit4ClassRunner.class) public class SampleRestAssuredApplicationTests { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - + private RequestSpecification documentationSpec; - - @Value("${local.server.port}") + + @LocalServerPort private int port; @Before @@ -59,7 +58,7 @@ public void setUp() { public void sample() throws Exception { given(this.documentationSpec) .accept("text/plain") - .filter(document("sample", + .filter(document("sample", preprocessRequest(modifyUris() .scheme("https") .host("api.example.com") diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 157e71255..d65d9d483 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 61564aca9..073af2bcd 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -28,10 +28,11 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.templates.TemplateFormats.markdown; + import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -43,21 +44,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSlate.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 022c19226..3e6285ba0 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,14 +11,13 @@ org.springframework.boot spring-boot-starter-parent - 1.3.6.RELEASE + 1.4.1.RELEASE UTF-8 1.7 - 4.3.1.RELEASE 1.2.0.BUILD-SNAPSHOT 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 bfeeb6269..fe4367311 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 @@ -44,21 +44,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 407a8119c..543781b72 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -38,11 +38,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -52,11 +51,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class GettingStartedDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a44f3f942..0306858fe 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -30,7 +30,6 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { 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 55b1e0368..8b7907e72 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 @@ -49,15 +49,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.StringUtils; @@ -65,9 +64,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index e147deeb4..101fc0d36 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -41,11 +41,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -55,11 +54,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class GettingStartedDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 3be74243e..3b9abc857 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -30,7 +30,6 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { @@ -38,11 +37,11 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-web' - testCompile('org.springframework:spring-test') { + testCompile('org.springframework.boot:spring-boot-starter-test') { exclude group: 'junit', module: 'junit;' } - testCompile 'org.testng:testng:6.9.10' testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' + testCompile 'org.testng:testng:6.9.10' } test { diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java index 9c4a9c9b6..3a0e58e48 100644 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -24,10 +24,9 @@ import java.lang.reflect.Method; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,8 +34,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -@SpringApplicationConfiguration(classes=SampleTestNgApplication.class) -@WebAppConfiguration +@SpringBootTest public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index a861c7e51..a5cb2d1d5 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,10 +1,5 @@ description = 'Spring REST Docs REST Assured' -boolean isPlatformUsingBootOneFour() { - def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] - bootVersion.startsWith('1.4') -} - dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' @@ -12,20 +7,12 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' - testCompile 'org.springframework:spring-test' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.4.1.RELEASE' + testCompile 'org.springframework.boot:spring-boot-test:1.4.1.RELEASE' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' } test { jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" -} - -afterEvaluate { - if (project.hasProperty('platformVersion') && platformUsingBootOneFour) { - dependencies { - springIoTestRuntime 'org.springframework.boot:spring-boot-test' - } - } -} +} \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 9c4f7615e..562a82603 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -33,18 +33,16 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -57,10 +55,8 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class RestAssuredRequestConverterTests { @Rule @@ -68,7 +64,7 @@ public class RestAssuredRequestConverterTests { private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); - @Value("${local.server.port}") + @LocalServerPort private int port; @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 6d1a84e37..da43824bc 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -32,17 +32,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -85,10 +83,8 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class RestAssuredRestDocumentationIntegrationTests { @Rule From dec3727da1c6a333bb17db4cfc8f15bb7d3bf99d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 31 Oct 2016 12:46:36 +0000 Subject: [PATCH 0359/1059] Add support for documenting body of a request, response or request part Closes gh-318 Closes gh-319 --- .../docs/asciidoc/documenting-your-api.adoc | 178 ++++++++++++++---- docs/src/docs/asciidoc/getting-started.adoc | 4 +- docs/src/test/java/com/example/Payload.java | 6 +- .../java/com/example/mockmvc/Payload.java | 93 +++++---- .../example/mockmvc/RequestPartPayload.java | 21 ++- .../java/com/example/restassured/Payload.java | 18 +- .../restassured/RequestPartPayload.java | 19 +- .../restdocs/config/SnippetConfigurer.java | 4 +- .../restdocs/payload/AbstractBodySnippet.java | 125 ++++++++++++ .../payload/PayloadDocumentation.java | 158 ++++++++++++++++ .../restdocs/payload/RequestBodySnippet.java | 84 +++++++++ .../payload/RequestPartBodySnippet.java | 111 +++++++++++ .../restdocs/payload/ResponseBodySnippet.java | 84 +++++++++ .../asciidoctor/default-request-body.snippet | 4 + .../default-request-part-body.snippet | 4 + .../asciidoctor/default-response-body.snippet | 4 + .../markdown/default-request-body.snippet | 3 + .../default-request-part-body.snippet | 3 + .../markdown/default-response-body.snippet | 3 + .../restdocs/AbstractSnippetTests.java | 6 +- .../RestDocumentationConfigurerTests.java | 10 +- .../payload/RequestBodyPartSnippetTests.java | 95 ++++++++++ .../payload/RequestBodySnippetTests.java | 92 +++++++++ .../payload/ResponseBodySnippetTests.java | 92 +++++++++ .../restdocs/test/SnippetMatchers.java | 13 +- .../request-body-with-language.snippet | 4 + .../request-part-body-with-language.snippet | 4 + .../response-body-with-language.snippet | 4 + .../request-body-with-language.snippet | 3 + .../request-part-body-with-language.snippet | 3 + .../response-body-with-language.snippet | 3 + 31 files changed, 1160 insertions(+), 95 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index a1eeb908f..e6068ce7f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -105,12 +105,28 @@ descriptors to a snippet that's preconfigured to ignore certain links. For examp include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] ---- + + [[documenting-your-api-request-response-payloads]] === Request and response payloads In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. Consider the following payload: +provided. + +By default, Spring REST Docs will automatically generate snippets for the body of the +request and the body of the response. These snippets are named `request-body.adoc` and +`response-body.adoc` respectively. + + + +[[documenting-your-api-request-response-payloads-fields]] +==== Request and response fields + +To provide more detailed documentation of a request or response payload, support for +documenting the payload's fields is provided. + +Consider the following payload: [source,json,indent=0] ---- @@ -122,7 +138,7 @@ provided. Consider the following payload: } ---- -It can be documented like this: +Its fields can be documented like this: [source,java,indent=0,role="primary"] .MockMvc @@ -195,11 +211,11 @@ TIP: By default, Spring REST Docs will assume that the payload you are documenti JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. -[[documenting-your-api-request-response-payloads-json]] -==== JSON payloads +[[documenting-your-api-request-response-payloads-fields-json]] +===== Fields in JSON payloads -[[documenting-your-api-request-response-payloads-json-field-paths]] -===== JSON field paths +[[documenting-your-api-request-response-payloads-fields-json-field-paths]] +====== JSON field paths JSON field paths use either dot notation or bracket notation. Dot notation uses '.' to separate each key in the path; `a.b`, for example. Bracket notation wraps each key in @@ -288,8 +304,8 @@ found in the following array: -[[documenting-your-api-request-response-payloads-json-field-types]] -===== JSON field types +[[documenting-your-api-request-response-payloads-fields-json-field-types]] +====== JSON field types When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: @@ -340,18 +356,18 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] <1> Set the field's type to `string`. -[[documenting-your-api-request-response-payloads-xml]] -==== XML payloads +[[documenting-your-api-request-response-payloads-fields-xml]] +===== XML payloads -[[documenting-your-api-request-response-payloads-xml-field-paths]] -===== XML field paths +[[documenting-your-api-request-response-payloads-fields-xml-field-paths]] +====== XML field paths XML field paths are described using XPath. `/` is used to descend into a child node. -[[documenting-your-api-request-response-payloads-xml-field-types]] -===== XML field types +[[documenting-your-api-request-response-payloads-fields-xml-field-types]] +====== XML field types When documenting an XML payload, you must provide a type for the field using the `type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` @@ -359,8 +375,8 @@ method will be used in the documentation. -[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] -==== Reusing field descriptors +[[documenting-your-api-request-response-payloads-fields-reusing-field-descriptors]] +===== Reusing field descriptors In addition to the general support for <>, the request and response snippets allow additional descriptors to be @@ -445,7 +461,10 @@ If a payload is large or structurally complex, it can be useful to document individual sections of the payload. REST Docs allows you to do so by extracting a subsection of the payload and then documenting it. -Consider the following JSON response payload: +[[documenting-your-api-request-response-payloads-subsections-body]] +===== Documenting a subsection of a request or response body + +Consider the following JSON response body: [source,json,indent=0] ---- @@ -463,13 +482,64 @@ Consider the following JSON response payload: } ---- -A snippet that documents the fields of the `temperature` object (`high` and `low`) can -be produced as follows: +A snippet that documents the `temperature` object can be produces as follows: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +The result is a snippet with the following contents: + +[source,json,indent=0] +---- + { + "temperature": { + "high": 21.2, + "low": 14.8 + } + } +---- + +To make the snippet's name distinct, an identifier for the subsection is included. By +default, this identifier is `beneath-${path}`. For example, the code above will result in +a snippet named `response-body-beneath-weather.temperature.adoc`. The identifier can +be customized using the `withSubsectionId(String)` method: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `request-body-temp.adoc`. + + + +[[documenting-your-api-request-response-payloads-subsections-fields]] +===== Documenting the fields of a subsection of a request or response + +As well as documenting a subsection of a request or response body, it's also possible to +document the fields in a particular subsection. A snippet that documents the fields of the `temperature` object (`high` and `low`) can be produced as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=fields-subsection] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -479,7 +549,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-subsection] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -490,15 +560,7 @@ The result is a snippet that contains a table describing the `high` and `low` fi of `weather.temperature`. To make the snippet's name distinct, an identifier for the subsection is included. By default, this identifier is `beneath-${path}`. For example, the code above will result in a snippet named -`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using -the `withSubsectionId(String)` method: - -[source,java,indent=0] ----- -include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] ----- - -This example will result in a snippet named `response-fields-temp.adoc`. +`response-fields-beneath-weather.temperature.adoc`. @@ -679,12 +741,51 @@ above. === Request part payloads The payload of a request part can be documented in much the same way as the -<>: +<> with support +for documenting a request part's body and its fields. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's body + +A snippet containing the body of a request part can be generated: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the + of the request part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + payload. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request + part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + +The result is a snippet `request-part-${part-name}-body.adoc` that contains the part's +body. For example, documenting a part named `metadata` will produce a snippet named +`request-part-metadata-body.adoc`. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's fields + +A request part's fields can be documented in much the same way as the fields of a request +or response: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -696,7 +797,7 @@ include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -930,6 +1031,13 @@ documented | `http-response.adoc` | Contains the HTTP response that was returned + +| `request-body.adoc` +| Contains the body of the request that was sent + +| `response-body.adoc` +| Contains the body of the response that was returned + |=== You can configure which snippets are produced by default. Please refer to the @@ -1076,6 +1184,4 @@ and adds a title: ---- <1> Add a title to the table <2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table - - +<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index d70a78ce7..f84f0f660 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -406,12 +406,14 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, four snippets are written: +By default, six snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` * `/index/httpie-request.adoc` + * `/index/request-body.adoc` + * `/index/response-body.adoc` Refer to <> for more information about these and other snippets that can be produced by Spring REST Docs. diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index f553927df..eb98f69fa 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -20,7 +20,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; public class Payload { @@ -35,9 +35,7 @@ public void bookFieldDescriptors() { public void customSubsectionId() { // tag::custom-subsection-id[] - responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), - fieldWithPath("high").description("…"), - fieldWithPath("low").description("…")); + responseBody(beneathPath("weather.temperature").withSubsectionId("temp")); // end::custom-subsection-id[] } diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 5802e2602..d1b12200e 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -26,9 +26,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,46 +41,50 @@ public class Payload { public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - fieldWithPath("contact.email").description("The user's email address"), // <2> - fieldWithPath("contact.name").description("The user's name")))); // <3> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + fieldWithPath("contact.email") + .description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - subsectionWithPath("contact").description("The user's contact details")))); // <1> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + subsectionWithPath("contact") + .description("The user's contact details")))); // <1> // end::subsection[] } public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::explicit-type[] - .andDo(document("index", responseFields( - fieldWithPath("contact.email") - .type(JsonFieldType.STRING) // <1> - .description("The user's email address")))); - // end::explicit-type[] + .andExpect(status().isOk()) + // tag::explicit-type[] + .andDo(document("index", + responseFields( + fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))); + // end::explicit-type[] } public void constraints() throws Exception { this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::constraints[] - .andDo(document("create-user", requestFields( - attributes(key("title").value("Fields for user creation")), // <1> - fieldWithPath("name").description("The user's name") - .attributes(key("constraints") - .value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email").description("The user's email address") - .attributes(key("constraints") - .value("Must be a valid email address"))))); // <3> - // end::constraints[] + .andExpect(status().isOk()) + // tag::constraints[] + .andDo(document("create-user", requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))); // <3> + // end::constraints[] } public void descriptorReuse() throws Exception { @@ -89,27 +94,39 @@ public void descriptorReuse() throws Exception { // tag::single-book[] this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("book", responseFields(book))); // <1> + .andExpect(status().isOk()).andDo(document("book", responseFields(book))); // <1> // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("book", responseFields( - fieldWithPath("[]").description("An array of books")) // <1> - .andWithPrefix("[].", book))); // <2> + .andDo(document("book", + responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> - fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> - fieldWithPath("low").description("The forecast low in degrees celcius")))); - // end::beneath-path[] + .andDo(document("location", + responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description( + "The forecast high in degrees celcius"), // <2> + fieldWithPath("low") + .description("The forecast low in degrees celcius")))); + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("location", + responseBody(beneathPath("weather.temperature")))); // <1> + + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java index 56d6ae85d..e020a4e73 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -23,6 +23,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,8 +31,8 @@ public class RequestPartPayload { private MockMvc mockMvc; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); MockMultipartFile metadata = new MockMultipartFile("metadata", "", @@ -42,7 +43,21 @@ public void response() throws Exception { .andExpect(status().isOk()) .andDo(document("image-upload", requestPartFields("metadata", // <1> fieldWithPath("version").description("The version of the image")))); // <2> - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartBody("metadata"))); // <1> + // end::body[] } } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index f7789cc50..b538acbb0 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -26,6 +26,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -46,7 +47,7 @@ public void response() throws Exception { .then().assertThat().statusCode(is(200)); // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] RestAssured.given(this.spec).accept("application/json") @@ -107,15 +108,24 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] RestAssured.given(this.spec).accept("application/json") .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))) .when().get("/locations/1") .then().assertThat().statusCode(is(200)); - // end::beneath-path[] + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java index 5dc8eda54..ec83908ef 100644 --- a/docs/src/test/java/com/example/restassured/RequestPartPayload.java +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -25,6 +25,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -32,8 +33,8 @@ public class RequestPartPayload { private RequestSpecification spec; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] Map metadata = new HashMap<>(); metadata.put("version", "1.0"); RestAssured.given(this.spec).accept("application/json") @@ -42,7 +43,19 @@ public void response() throws Exception { .when().multiPart("image", new File("image.png"), "image/png") .multiPart("metadata", metadata).post("images") .then().assertThat().statusCode(is(200)); - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) // <1> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::body[] } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 0da0b48c9..9e96055cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; +import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; @@ -42,7 +43,8 @@ public abstract class SnippetConfigurer private List defaultSnippets = new ArrayList<>(Arrays.asList( CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse(), + PayloadDocumentation.requestBody(), PayloadDocumentation.responseBody())); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java new file mode 100644 index 000000000..0010ea28e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014-2016 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.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.ModelCreationException; +import org.springframework.restdocs.snippet.TemplatedSnippet; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response body. + * + * @author Andy Wilkinson + */ +public abstract class AbstractBodySnippet extends TemplatedSnippet { + + private final PayloadSubsectionExtractor subsectionExtractor; + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + this(type, type, subsectionExtractor, attributes); + } + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param name the name of the snippet + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String name, String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super(name + "-body" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-body", attributes); + this.subsectionExtractor = subsectionExtractor; + } + + @Override + protected Map createModel(Operation operation) { + try { + MediaType contentType = getContentType(operation); + byte[] content = getContent(operation); + if (this.subsectionExtractor != null) { + content = this.subsectionExtractor.extractSubsection(content, + contentType); + } + Charset charset = extractCharset(contentType); + String body = charset != null ? new String(content, charset) + : new String(content); + Map model = new HashMap<>(); + model.put("body", body); + return model; + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + } + + private Charset extractCharset(MediaType contentType) { + if (contentType == null) { + return null; + } + return contentType.getCharset(); + } + + /** + * Returns the content of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content + * @throws IOException if the content cannot be extracted + */ + protected abstract byte[] getContent(Operation operation) throws IOException; + + /** + * Returns the content type of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content type + */ + protected abstract MediaType getContentType(Operation operation); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 3f31ef76e..7ecbf9ebc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -1443,6 +1443,164 @@ public static ResponseFieldsSnippet relaxedResponseFields( true); } + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. + * + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody() { + return new RequestBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody(Map attributes) { + return new RequestBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. + * + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody() { + return new ResponseBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody(Map attributes) { + return new ResponseBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new ResponseBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new ResponseBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. + * + * @param partName the name of the request part + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName) { + return new RequestPartBodySnippet(partName); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. The given attributes will be made available during + * snippet generation. + * + * @param partName the name of the request part + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + Map attributes) { + return new RequestPartBodySnippet(partName, attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestPartBodySnippet(partName, subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. The given attributes will be made available + * during snippet generation. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestPartBodySnippet(partName, subsectionExtractor, attributes); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java new file mode 100644 index 000000000..5c6d3ea2e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2016 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.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a request. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code RequestBodySnippet}. + */ + public RequestBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public RequestBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getRequest().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getRequest().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java new file mode 100644 index 000000000..0e5ec3243 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java @@ -0,0 +1,111 @@ +/* + * Copyright 2014-2016 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.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the body of a request part. + * + * @author Andy Wilkinson + */ +public class RequestPartBodySnippet extends AbstractBodySnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. + * + * @param partName the name of the request part + */ + public RequestPartBodySnippet(String partName) { + this(partName, null, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the subsection of + * the body of the request part with the given {@code partName} extracted by the given + * {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + this(partName, subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The given additional + * {@code attributes} will be included in the model during template rendering. + * + * @param partName the name of the request part + * @param attributes the additional attributes + */ + public RequestPartBodySnippet(String partName, Map attributes) { + this(partName, null, attributes); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The subsection of the body extracted + * by the given {@code subsectionExtractor} will be documented and the given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request-part-" + partName, "request-part", subsectionExtractor, + attributes); + this.partName = partName; + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java new file mode 100644 index 000000000..d0dbe1400 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2016 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.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a response. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code ResponseBodySnippet}. + */ + public ResponseBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public ResponseBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("response", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getResponse().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getResponse().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index dbca4c0e9..014caa4d7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -68,7 +68,11 @@ public AbstractSnippetTests(String name, TemplateFormat templateFormat) { } public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.templateFormat, language); + return this.codeBlock(language, null); + } + + public CodeBlockMatcher codeBlock(String language, String options) { + return SnippetMatchers.codeBlock(this.templateFormat, language, options); } public TableMatcher tableWithHeader(String... headers) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 995a6ffb6..4cc0b635f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -31,6 +31,8 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.payload.RequestBodySnippet; +import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -76,7 +78,9 @@ public void defaultConfiguration() { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -138,7 +142,9 @@ public void additionalDefaultSnippets() { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class), equalTo(snippet))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java new file mode 100644 index 000000000..f19daee43 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014-2016 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodyPartSnippetTests extends AbstractSnippetTests { + + public RequestBodyPartSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestPartWithBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", "some content".getBytes()).build()); + } + + @Test + public void requestPartWithNoBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", new byte[0]).build()); + } + + @Test + public void subsectionOfRequestPartBody() throws IOException { + this.snippets.expect("request-part-one-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestPartBody("one", beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\":{\"b\":{\"c\":5}}}".getBytes()).build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-part-body")) + .willReturn(snippetResource("request-part-body-with-language")); + requestPartBody("one", + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("one", + "{\"a\":\"alpha\"}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java new file mode 100644 index 000000000..e6bde76d8 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014-2016 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippetTests extends AbstractSnippetTests { + + public RequestBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestWithBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestBody().document(this.operationBuilder.request("http://localhost") + .content("some content").build()); + } + + @Test + public void requestWithNoBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestBody().document(this.operationBuilder.request("http://localhost").build()); + } + + @Test + public void subsectionOfRequestBody() throws IOException { + this.snippets.expect("request-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestBody(beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-body")) + .willReturn(snippetResource("request-body-with-language")); + requestBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\":\"alpha\"}").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java new file mode 100644 index 000000000..2b42539c0 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014-2016 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link ResponseBodySnippet}. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippetTests extends AbstractSnippetTests { + + public ResponseBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void responseWithBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + PayloadDocumentation.responseBody().document( + this.operationBuilder.response().content("some content").build()); + } + + @Test + public void responseWithNoBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("")); + PayloadDocumentation.responseBody() + .document(this.operationBuilder.response().build()); + } + + @Test + public void subsectionOfResponseBody() throws IOException { + this.snippets.expect("response-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + responseBody(beneathPath("a.b")).document(this.operationBuilder.response() + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-body")) + .willReturn(snippetResource("response-body-with-language")); + responseBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().content("{\"a\":\"alpha\"}") + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 287c933f8..c5e8da20d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -95,6 +95,15 @@ public static CodeBlockMatcher codeBlock(TemplateFormat format, String langua return new MarkdownCodeBlockMatcher(language); } + @SuppressWarnings({ "rawtypes" }) + public static CodeBlockMatcher codeBlock(TemplateFormat format, String language, + String options) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockMatcher(language, options); + } + return new MarkdownCodeBlockMatcher(language); + } + private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { @@ -186,7 +195,7 @@ public static class AsciidoctorCodeBlockMatcher Date: Fri, 16 Dec 2016 14:58:06 +0100 Subject: [PATCH 0360/1059] Clarify that parameterized output directory uses the simple class name Closes gh-334 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442b..016aab3f6 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -805,13 +805,13 @@ are supported: | The name of the test method, formatted using snake_case | {ClassName} -| The unmodified name of the test class +| The unmodified simple name of the test class | {class-name} -| The name of the test class, formatted using kebab-case +| The simple name of the test class, formatted using kebab-case | {class_name} -| The nome of the test class, formatted using snake_case +| The simple name of the test class, formatted using snake_case | {step} | The count of calls made to the service in the current test @@ -924,6 +924,4 @@ and adds a title: ---- <1> Add a title to the table <2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table - - +<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file From 2fb3ede3a6073f8ca702dff1098298d5eddf3261 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 3 Nov 2016 06:40:59 +0100 Subject: [PATCH 0361/1059] Document that port is omitted when it matches the scheme's default Closes gh-325 Closes gh-329 --- docs/src/docs/asciidoc/configuration.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 00c31ac61..178ac7b07 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -34,6 +34,9 @@ to change one or more of the defaults to suit your needs: include::{examples-dir}/com/example/mockmvc/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- +NOTE: If the port is set to the default for the configured scheme (port 80 for HTTP or +port 443 for HTTPS), it will be omitted from any URIs in the generated snippets. + TIP: To configure a request's context path, use the `contextPath` method on `MockHttpServletRequestBuilder`. From 677a23647fdd2502843f296fbd9cc1b2f2dd45d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Jan 2017 19:57:29 +0000 Subject: [PATCH 0362/1059] Compile and test against Spring Framework 4.3.6.RELEASE This commit upgrade to Spring Framework 4.3.6.RELEASE. A side-effect of this change is that the Content-Type header is now always the first header when building a request with MockMvc. One test has been updated to reflect this change in ordering. Closes gh-342 --- build.gradle | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 7750213db..b4f341154 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.3.1.RELEASE' + springVersion = '4.3.6.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 26ea2c958..cd47c51fe 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -401,26 +401,30 @@ public void preprocessedRequest() throws Exception { assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("a", "alpha").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) + is(snippet( + asciidoctor()) + .withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("Content-Type", + "application/json") + .header("a", "alpha").header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) .header("Host", "localhost:8080") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + assertThat(new File( + "build/generated-snippets/preprocessed-request/http-request.adoc"), is( + snippet(asciidoctor()) + .withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("Content-Type", + "application/json") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test From 7e4a865d1bccb38286ee053b7e914609deda6df2 Mon Sep 17 00:00:00 2001 From: Florian Benz Date: Mon, 30 Jan 2017 12:34:25 +0100 Subject: [PATCH 0363/1059] Add spring-auto-restdocs to third-party extensions Closes gh-343 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c9b178feb..349347f8b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ There are several that you can contribute to Spring REST Docs: | ---- | ----------- | | [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | | [restdocsext-jersey][19] | Enables Spring REST Docs to be used with [Jersey's test framework][20] | +| [spring-auto-restdocs][21] | Uses introspection and Javadoc to automatically document request and response parameters | ## Licence @@ -63,4 +64,5 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [17]: https://github.com/ePages-de/restdocs-wiremock [18]: http://wiremock.org/ [19]: https://github.com/RESTDocsEXT/restdocsext-jersey -[20]: https://jersey.java.net/documentation/latest/test-framework.html \ No newline at end of file +[20]: https://jersey.java.net/documentation/latest/test-framework.html +[21]: https://github.com/ScaCap/spring-auto-restdocs \ No newline at end of file From 25bc6c37dae372a15dab59eee6acd252a0176968 Mon Sep 17 00:00:00 2001 From: Tomasz Kopczynski Date: Sun, 18 Dec 2016 15:43:58 +0100 Subject: [PATCH 0364/1059] Include cookies in request snippets Closes gh-336 See gh-302, gh-303, gh-304 --- .../restdocs/cli/CliOperationRequest.java | 7 ++++ .../restdocs/cli/CurlRequestSnippet.java | 18 +++++++++ .../restdocs/cli/HttpieRequestSnippet.java | 6 +++ .../restdocs/http/HttpRequestSnippet.java | 8 ++++ .../restdocs/operation/OperationRequest.java | 9 +++++ .../operation/OperationRequestFactory.java | 34 ++++++++++++++--- .../operation/StandardOperationRequest.java | 13 ++++++- .../restdocs/cli/CurlRequestSnippetTests.java | 11 ++++++ .../cli/HttpieRequestSnippetTests.java | 11 ++++++ .../headers/RequestHeadersSnippetTests.java | 7 +++- .../http/HttpRequestSnippetTests.java | 15 ++++++++ .../restdocs/test/OperationBuilder.java | 12 +++++- .../mockmvc/MockMvcRequestConverter.java | 15 +++++++- .../mockmvc/MockMvcRequestConverterTests.java | 23 ++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 37 +++++++++++++++++++ .../RestAssuredRequestConverter.java | 14 ++++++- .../RestAssuredRequestConverterTests.java | 23 ++++++++++++ ...uredRestDocumentationIntegrationTests.java | 19 ++++++++++ 18 files changed, 271 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 2dde09ef6..22a6b2a4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -25,6 +25,8 @@ import java.util.Map.Entry; import java.util.Set; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; @@ -125,6 +127,11 @@ public URI getUri() { return this.delegate.getUri(); } + @Override + public Collection getCookies() { + return this.delegate.getCookies(); + } + private interface HeaderFilter { boolean allow(String name, List value); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 4f8d603cf..508097906 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -92,12 +94,28 @@ private String getOptions(Operation operation) { writeUserOptionIfNecessary(request, printer); writeHttpMethodIfNecessary(request, printer); writeHeaders(request, printer); + writeCookies(request, printer); writePartsIfNecessary(request, printer); writeContent(request, printer); return command.toString(); } + private void writeCookies(CliOperationRequest request, PrintWriter printer) { + if (request.getCookies() != null && request.getCookies().size() > 0) { + printer.print(" --cookie "); + StringBuilder cookiesBuilder = new StringBuilder(); + + for (Cookie cookie : request.getCookies()) { + cookiesBuilder.append(String.format("%s=%s;", cookie.getName(), cookie.getValue())); + } + + String cookiesHeader = cookiesBuilder.substring(0, cookiesBuilder.length() - 1); // remove trailing semicolon + + printer.print(String.format("'%s'", cookiesHeader)); // add single quotes + } + } + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 10b96facb..6fa07f053 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -160,6 +162,10 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { writer.print(String.format(" '%s:%s'", entry.getKey(), header)); } } + + for (Cookie cookie : request.getCookies()) { + writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); + } } private void writeParametersIfNecessary(CliOperationRequest request, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 0110dd1b6..8262571ae 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -35,6 +37,7 @@ import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; + /** * A {@link Snippet} that documents an HTTP request. * @@ -112,6 +115,11 @@ private List> getHeaders(OperationRequest request) { } } + + for (Cookie cookie : request.getCookies()) { + headers.add(header(HttpHeaders.COOKIE, String.format("%s=%s", cookie.getName(), cookie.getValue()))); + } + if (requiresFormEncodingContentTypeHeader(request)) { headers.add(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 29153eec6..50d0e7bd9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -19,6 +19,8 @@ import java.net.URI; import java.util.Collection; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -86,4 +88,11 @@ public interface OperationRequest { */ URI getUri(); + /** + * Returns cookies sent with the request. + * + * @return the cookies + */ + Collection getCookies(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 198011b45..3f75e289b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -18,6 +18,9 @@ import java.net.URI; import java.util.Collection; +import java.util.Collections; + +import javax.servlet.http.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -40,13 +43,34 @@ public class OperationRequestFactory { * @param headers the request's headers * @param parameters the request's parameters * @param parts the request's parts + * @param cookies the request's cookies * @return the {@code OperationRequest} */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts) { + Collection parts, + Collection cookies) { return new StandardOperationRequest(uri, method, content, - augmentHeaders(headers, uri, content), parameters, parts); + augmentHeaders(headers, uri, content), parameters, parts, cookies); + } + + /** + * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented + * to ensure that they always include a {@code Content-Length} header if the request + * has any content and a {@code Host} header. + * + * @param uri the request's uri + * @param method the request method + * @param content the content of the request + * @param headers the request's headers + * @param parameters the request's parameters + * @param parts the request's parts + * @return the {@code OperationRequest} + */ + public OperationRequest create(URI uri, HttpMethod method, byte[] content, + HttpHeaders headers, Parameters parameters, + Collection parts) { + return create(uri, method, content, headers, parameters, parts, Collections.emptyList()); } /** @@ -62,7 +86,7 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, public OperationRequest createFrom(OperationRequest original, byte[] newContent) { return new StandardOperationRequest(original.getUri(), original.getMethod(), newContent, getUpdatedHeaders(original.getHeaders(), newContent), - original.getParameters(), original.getParts()); + original.getParameters(), original.getParts(), original.getCookies()); } /** @@ -78,7 +102,7 @@ public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, original.getParameters(), - original.getParts()); + original.getParts(), original.getCookies()); } /** @@ -94,7 +118,7 @@ public OperationRequest createFrom(OperationRequest original, Parameters newParameters) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), original.getHeaders(), newParameters, - original.getParts()); + original.getParts(), original.getCookies()); } private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index a09bd645f..594534ce4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.Collections; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -39,6 +41,8 @@ class StandardOperationRequest extends AbstractOperationMessage private URI uri; + private Collection cookies; + /** * Creates a new request with the given {@code uri} and {@code method}. The request * will have the given {@code headers}, {@code parameters}, and {@code parts}. @@ -52,12 +56,14 @@ class StandardOperationRequest extends AbstractOperationMessage */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts) { + Collection parts, + Collection cookies) { super(content, headers); this.uri = uri; this.method = method; this.parameters = parameters; this.parts = parts; + this.cookies = cookies; } @Override @@ -80,4 +86,9 @@ public URI getUri() { return this.uri; } + @Override + public Collection getCookies() { + return this.cookies; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index fc4cf64c7..b5a23a5a9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -251,6 +251,17 @@ public void requestWithHeaders() throws IOException { .header("a", "alpha").build()); } + @Test + public void requestWithCookies() throws IOException { + this.snippets.expectCurlRequest() + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " --cookie 'name1=value1;name2=value2'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2").build()); + } + @Test public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index d5dc8a399..8cf564d58 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -252,6 +252,17 @@ public void requestWithHeaders() throws IOException { .header("a", "alpha").build()); } + @Test + public void requestWithCookies() throws IOException { + this.snippets.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Cookie:name1=value1' 'Cookie:name2=value2'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2").build()); + } + @Test public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index ef87c2f72..d5c73bcb0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -54,7 +54,7 @@ public void requestWithHeaders() throws IOException { .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six")); + .row("`Connection`", "six").row("`Cookie`", "seven")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -63,7 +63,8 @@ public void requestWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName( "Connection") - .description("six"))) + .description("six"), + headerWithName("Cookie").description("seven"))) .document( this.operationBuilder .request( @@ -78,6 +79,8 @@ public void requestWithHeaders() throws IOException { "max-age=0") .header("Connection", "keep-alive") + .header("Cookie", + "cookie1=cookieVal1; cookie2=cookieVal2") .build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 88c27ee1e..2e64467e2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -80,6 +80,21 @@ public void getRequestWithPort() throws IOException { .request("http://localhost:8080/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithCookies() throws IOException { + this.snippets.expectHttpRequest() + .withContents(httpRequest(RequestMethod.GET, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.COOKIE, "name1=value1") + .header(HttpHeaders.COOKIE, "name2=value2")); + + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2") + .build()); + } + @Test public void getRequestWithQueryString() throws IOException { this.snippets.expectHttpRequest() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 698a80dac..4bfa58af3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -19,11 +19,14 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.servlet.http.Cookie; + import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -153,6 +156,8 @@ public final class OperationRequestBuilder { private List partBuilders = new ArrayList<>(); + private Collection cookies = new ArrayList<>(); + private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); } @@ -163,7 +168,7 @@ private OperationRequest buildRequest() { parts.add(builder.buildPart()); } return new OperationRequestFactory().create(this.requestUri, this.method, - this.content, this.headers, this.parameters, parts); + this.content, this.headers, this.parameters, parts, this.cookies); } public Operation build() { @@ -209,6 +214,11 @@ public OperationRequestPartBuilder part(String name, byte[] content) { return partBuilder; } + public OperationRequestBuilder cookie(String name, String value) { + this.cookies.add(new Cookie(name, value)); + return this; + } + /** * Basic builder API for creating an {@link OperationRequestPart}. */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index e933a0e0a..2d86da0a6 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -21,10 +21,14 @@ import java.io.StringWriter; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map.Entry; import javax.servlet.ServletException; +import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.springframework.http.HttpHeaders; @@ -67,6 +71,7 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); + Collection cookies = extractCookies(mockRequest); String queryString = mockRequest.getQueryString(); if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { @@ -78,13 +83,21 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { ? "?" + queryString : "")), HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, - parameters, parts); + parameters, parts, cookies); } catch (Exception ex) { throw new ConversionException(ex); } } + private Collection extractCookies(MockHttpServletRequest mockRequest) { + if (mockRequest.getCookies() != null) { + return Arrays.asList(mockRequest.getCookies()); + } + + return Collections.emptyList(); + } + private List extractParts(MockHttpServletRequest servletRequest) throws IOException, ServletException { List parts = new ArrayList<>(); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 9304dd515..501752a57 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -19,7 +19,9 @@ import java.io.ByteArrayInputStream; import java.net.URI; import java.util.Arrays; +import java.util.Iterator; +import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.junit.Test; @@ -88,6 +90,27 @@ public void requestWithHeaders() throws Exception { assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo"))); } + @Test + public void requestWithCookies() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo") + .cookie(new Cookie("cookieName1", "cookieVal1"), + new Cookie("cookieName2", "cookieVal2"))); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getCookies().size(), is(equalTo(2))); + + Iterator cookieIterator = request.getCookies().iterator(); + + Cookie cookie1 = cookieIterator.next(); + assertThat(cookie1.getName(), is(equalTo("cookieName1"))); + assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); + + Cookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName(), is(equalTo("cookieName2"))); + assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); + } + @Test public void httpsRequest() throws Exception { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index cd47c51fe..a7e563140 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.regex.Pattern; +import javax.servlet.http.Cookie; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -159,6 +161,23 @@ public void curlSnippetWithContent() throws Exception { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithCookies() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/") + .accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()).andDo(document("curl-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ curl " + "'http://localhost:8080/' -i " + + "-H 'Accept: application/json' --cookie 'cookieName=cookieVal'")))); + } + @Test public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -207,6 +226,24 @@ public void httpieSnippetWithContent() throws Exception { + " 'Accept:application/json'")))); } + @Test + public void httpieSnippetWithCookies() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/") + .accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-cookies/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ http GET 'http://localhost:8080/'" + + " 'Accept:application/json' 'Cookie:cookieName=cookieVal'")))); + } + @Test public void httpieSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 1673366a0..eff63a4c2 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map.Entry; +import com.jayway.restassured.response.Cookie; import com.jayway.restassured.response.Header; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.MultiPartSpecification; @@ -55,7 +56,18 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), HttpMethod.valueOf(requestSpec.getMethod().name()), extractContent(requestSpec), extractHeaders(requestSpec), - extractParameters(requestSpec), extractParts(requestSpec)); + extractParameters(requestSpec), extractParts(requestSpec), + extractCookies(requestSpec)); + } + + private Collection extractCookies(FilterableRequestSpecification requestSpec) { + Collection cookies = new ArrayList<>(); + + for (Cookie cookie : requestSpec.getCookies()) { + cookies.add(new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + } + + return cookies; } private byte[] extractContent(FilterableRequestSpecification requestSpec) { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 562a82603..4db143211 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -25,6 +25,8 @@ import java.util.Collection; import java.util.Iterator; +import javax.servlet.http.Cookie; + import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; @@ -143,6 +145,27 @@ public void headers() { is(equalTo(Arrays.asList("localhost:" + this.port)))); } + @Test + public void cookies() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .cookie("cookie1", "cookieVal1") + .cookie("cookie2", "cookieVal2"); + requestSpec.get("/"); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getCookies().size(), is(equalTo(2))); + + Iterator cookieIterator = request.getCookies().iterator(); + Cookie cookie1 = cookieIterator.next(); + + assertThat(cookie1.getName(), is(equalTo("cookie1"))); + assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); + + Cookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName(), is(equalTo("cookie2"))); + assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); + } + @Test public void multipart() { RequestSpecification requestSpec = RestAssured.given().port(this.port) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index da43824bc..087003d90 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -119,6 +119,25 @@ public void curlSnippetWithContent() throws Exception { + "-d 'content'")))); } + @Test + public void curlSnippetWithCookies() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-cookies")).accept("application/json") + .contentType(contentType) + .cookie("cookieName", "cookieVal").get("/") + .then() + .statusCode(200); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ curl 'http://localhost:" + this.port + "/' -i " + + "-H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "--cookie 'cookieName=cookieVal'")))); + } + @Test public void curlSnippetWithQueryStringOnPost() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) From a9c1e04081971d06eacef8109a47688e72753420 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 2 Feb 2017 14:42:43 +0000 Subject: [PATCH 0365/1059] Polish "Include cookies in request snippets" See gh-336 Closes gh-302 Closes gh-303 Closes gh-304 --- .../restdocs/cli/CliOperationRequest.java | 2 +- .../restdocs/cli/CurlRequestSnippet.java | 18 ++++---- .../restdocs/cli/HttpieRequestSnippet.java | 8 +++- .../restdocs/operation/OperationRequest.java | 8 ++-- .../operation/StandardOperationRequest.java | 9 ++-- .../restdocs/cli/CurlRequestSnippetTests.java | 34 +++++++-------- .../cli/HttpieRequestSnippetTests.java | 34 +++++++-------- .../headers/RequestHeadersSnippetTests.java | 21 ++++------ .../http/HttpRequestSnippetTests.java | 41 +++++++++---------- .../restdocs/test/OperationBuilder.java | 2 +- .../mockmvc/MockMvcRequestConverter.java | 2 +- .../mockmvc/MockMvcRequestConverterTests.java | 5 +-- ...kMvcRestDocumentationIntegrationTests.java | 13 +++--- .../RestAssuredRequestConverter.java | 10 ++--- .../RestAssuredRequestConverterTests.java | 5 +-- ...uredRestDocumentationIntegrationTests.java | 12 +++--- 16 files changed, 107 insertions(+), 117 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 22a6b2a4c..10ae0486c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 508097906..886a4c070 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -32,6 +32,7 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -102,17 +103,16 @@ private String getOptions(Operation operation) { } private void writeCookies(CliOperationRequest request, PrintWriter printer) { - if (request.getCookies() != null && request.getCookies().size() > 0) { - printer.print(" --cookie "); + if (!CollectionUtils.isEmpty(request.getCookies())) { StringBuilder cookiesBuilder = new StringBuilder(); - for (Cookie cookie : request.getCookies()) { - cookiesBuilder.append(String.format("%s=%s;", cookie.getName(), cookie.getValue())); + if (cookiesBuilder.length() > 0) { + cookiesBuilder.append(";"); + } + cookiesBuilder.append( + String.format("%s=%s", cookie.getName(), cookie.getValue())); } - - String cookiesHeader = cookiesBuilder.substring(0, cookiesBuilder.length() - 1); // remove trailing semicolon - - printer.print(String.format("'%s'", cookiesHeader)); // add single quotes + printer.print(String.format(" --cookie '%s'", cookiesBuilder.toString())); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 6fa07f053..38a8970be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -108,6 +108,7 @@ private String getRequestItems(CliOperationRequest request) { PrintWriter printer = new PrintWriter(requestItems); writeFormDataIfNecessary(request, printer); writeHeaders(request, printer); + writeCookies(request, printer); writeParametersIfNecessary(request, printer); return requestItems.toString(); } @@ -162,9 +163,12 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { writer.print(String.format(" '%s:%s'", entry.getKey(), header)); } } + } + private void writeCookies(OperationRequest request, PrintWriter writer) { for (Cookie cookie : request.getCookies()) { - writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); + writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), + cookie.getValue())); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 50d0e7bd9..93972dba0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2017 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. @@ -89,9 +89,11 @@ public interface OperationRequest { URI getUri(); /** - * Returns cookies sent with the request. + * Returns {@link Cookie Cookies} sent with the request. If no cookies were sent an + * empty collection is returned. * - * @return the cookies + * @return the cookies, never {@code null} + * @since 1.2.0 */ Collection getCookies(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 594534ce4..8495c6c99 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2017 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. @@ -45,7 +45,8 @@ class StandardOperationRequest extends AbstractOperationMessage /** * Creates a new request with the given {@code uri} and {@code method}. The request - * will have the given {@code headers}, {@code parameters}, and {@code parts}. + * will have the given {@code headers}, {@code parameters}, {@code parts}, and + * {@code cookies}. * * @param uri the uri * @param method the method @@ -53,11 +54,11 @@ class StandardOperationRequest extends AbstractOperationMessage * @param headers the headers * @param parameters the parameters * @param parts the parts + * @param cookies the cookies */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, - Collection cookies) { + Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index b5a23a5a9..33d6aa88e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -254,12 +254,11 @@ public void requestWithHeaders() throws IOException { @Test public void requestWithCookies() throws IOException { this.snippets.expectCurlRequest() - .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + - " --cookie 'name1=value1;name2=value2'")); + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " --cookie 'name1=value1;name2=value2'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2").build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -269,9 +268,10 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @@ -286,9 +286,9 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -302,8 +302,8 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -318,9 +318,9 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test @@ -332,7 +332,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 8cf564d58..acb12cb96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -255,12 +255,11 @@ public void requestWithHeaders() throws IOException { @Test public void requestWithCookies() throws IOException { this.snippets.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo'" + - " 'Cookie:name1=value1' 'Cookie:name2=value2'")); + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Cookie:name1=value1' 'Cookie:name2=value2'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2").build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -270,9 +269,10 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @@ -288,9 +288,9 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -304,8 +304,8 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -320,9 +320,9 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test @@ -334,7 +334,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index d5c73bcb0..1f28ea119 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -54,7 +54,7 @@ public void requestWithHeaders() throws IOException { .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six").row("`Cookie`", "seven")); + .row("`Connection`", "six")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -63,8 +63,7 @@ public void requestWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName( "Connection") - .description("six"), - headerWithName("Cookie").description("seven"))) + .description("six"))) .document( this.operationBuilder .request( @@ -73,15 +72,9 @@ public void requestWithHeaders() throws IOException { .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", - "en-US,en;q=0.5") - .header("Cache-Control", - "max-age=0") - .header("Connection", - "keep-alive") - .header("Cookie", - "cookie1=cookieVal1; cookie2=cookieVal2") - .build()); + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); } @Test @@ -150,7 +143,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .header("X-Test", "test") .header("Accept-Encoding", "gzip, deflate") - .header("Accept", "*/*").build()); + .header("Accept", "*/*").build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 2e64467e2..4312770ee 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -90,9 +90,7 @@ public void getRequestWithCookies() throws IOException { new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2") - .build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -142,8 +140,8 @@ public void postRequestWithContent() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").content(content).build()); @@ -154,8 +152,8 @@ public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -168,8 +166,8 @@ public void postRequestWithContentAndDisjointQueryStringAndParameters() String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?b=bravo") @@ -182,8 +180,8 @@ public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameter String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?b=bravo").method("POST") @@ -196,8 +194,8 @@ public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters( String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?b=bravo&a=alpha").method("POST") @@ -251,8 +249,8 @@ public void putRequestWithContent() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").content(content).build()); @@ -284,7 +282,7 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()).build()); + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -310,8 +308,8 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -347,9 +345,8 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .build()); + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 4bfa58af3..6d210b5f1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 2d86da0a6..2aeb6119a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 501752a57..d2a18373c 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -93,8 +93,7 @@ public void requestWithHeaders() throws Exception { @Test public void requestWithCookies() throws Exception { OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo") - .cookie(new Cookie("cookieName1", "cookieVal1"), + .get("/foo").cookie(new Cookie("cookieName1", "cookieVal1"), new Cookie("cookieName2", "cookieVal2"))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7e563140..77b2136a2 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -166,10 +166,9 @@ public void curlSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(get("/") - .accept(MediaType.APPLICATION_JSON) - .cookie(new Cookie("cookieName", "cookieVal"))) - .andExpect(status().isOk()).andDo(document("curl-snippet-with-cookies")); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-cookies")); assertThat( new File( "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), @@ -231,10 +230,8 @@ public void httpieSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(get("/") - .accept(MediaType.APPLICATION_JSON) - .cookie(new Cookie("cookieName", "cookieVal"))) - .andExpect(status().isOk()) + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))).andExpect(status().isOk()) .andDo(document("httpie-snippet-with-cookies")); assertThat( new File( diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index eff63a4c2..6b64c6a3c 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -60,13 +60,13 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { extractCookies(requestSpec)); } - private Collection extractCookies(FilterableRequestSpecification requestSpec) { + private Collection extractCookies( + FilterableRequestSpecification requestSpec) { Collection cookies = new ArrayList<>(); - for (Cookie cookie : requestSpec.getCookies()) { - cookies.add(new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + cookies.add( + new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); } - return cookies; } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 4db143211..4617cb30d 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -148,8 +148,7 @@ public void headers() { @Test public void cookies() { RequestSpecification requestSpec = RestAssured.given().port(this.port) - .cookie("cookie1", "cookieVal1") - .cookie("cookie2", "cookieVal2"); + .cookie("cookie1", "cookieVal1").cookie("cookie2", "cookieVal2"); requestSpec.get("/"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 087003d90..07d62682b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -124,17 +124,15 @@ public void curlSnippetWithCookies() throws Exception { String contentType = "text/plain; charset=UTF-8"; given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-cookies")).accept("application/json") - .contentType(contentType) - .cookie("cookieName", "cookieVal").get("/") - .then() - .statusCode(200); + .contentType(contentType).cookie("cookieName", "cookieVal").get("/") + .then().statusCode(200); assertThat( new File( "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") .content("$ curl 'http://localhost:" + this.port + "/' -i " - + "-H 'Accept: application/json' " - + "-H 'Content-Type: " + contentType + "' " + + "-H 'Accept: application/json' " + "-H 'Content-Type: " + + contentType + "' " + "--cookie 'cookieName=cookieVal'")))); } From 722048e20fe77956e4ddb1d148433c78b114acf3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Feb 2017 14:53:54 +0000 Subject: [PATCH 0366/1059] Introduce RequestCookie to avoid core depending on the Servlet API Closes gh-345 --- spring-restdocs-core/build.gradle | 1 - .../restdocs/cli/CliOperationRequest.java | 5 +- .../restdocs/cli/CurlRequestSnippet.java | 5 +- .../restdocs/cli/HttpieRequestSnippet.java | 5 +- .../restdocs/http/HttpRequestSnippet.java | 9 ++- .../restdocs/operation/OperationRequest.java | 8 +-- .../operation/OperationRequestFactory.java | 8 +-- .../restdocs/operation/RequestCookie.java | 60 +++++++++++++++++++ .../operation/StandardOperationRequest.java | 8 +-- .../restdocs/test/OperationBuilder.java | 7 +-- spring-restdocs-mockmvc/build.gradle | 1 + .../mockmvc/MockMvcRequestConverter.java | 19 +++--- .../mockmvc/MockMvcRequestConverterTests.java | 16 ++--- .../RestAssuredRequestConverter.java | 8 +-- .../RestAssuredRequestConverterTests.java | 9 ++- 15 files changed, 111 insertions(+), 58 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 4a8417072..60752f0b1 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -27,7 +27,6 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' compile 'org.springframework:spring-web' - compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 10ae0486c..36bcb5913 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -25,13 +25,12 @@ import java.util.Map.Entry; import java.util.Set; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.Base64Utils; /** @@ -128,7 +127,7 @@ public URI getUri() { } @Override - public Collection getCookies() { + public Collection getCookies() { return this.delegate.getCookies(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 886a4c070..1fb6140ac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -23,13 +23,12 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.CollectionUtils; @@ -105,7 +104,7 @@ private String getOptions(Operation operation) { private void writeCookies(CliOperationRequest request, PrintWriter printer) { if (!CollectionUtils.isEmpty(request.getCookies())) { StringBuilder cookiesBuilder = new StringBuilder(); - for (Cookie cookie : request.getCookies()) { + for (RequestCookie cookie : request.getCookies()) { if (cookiesBuilder.length() > 0) { cookiesBuilder.append(";"); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 38a8970be..410495fee 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -23,8 +23,6 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -32,6 +30,7 @@ import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -166,7 +165,7 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { } private void writeCookies(OperationRequest request, PrintWriter writer) { - for (Cookie cookie : request.getCookies()) { + for (RequestCookie cookie : request.getCookies()) { writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 8262571ae..f72337af3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -24,8 +24,6 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -33,11 +31,11 @@ import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; - /** * A {@link Snippet} that documents an HTTP request. * @@ -116,8 +114,9 @@ private List> getHeaders(OperationRequest request) { } } - for (Cookie cookie : request.getCookies()) { - headers.add(header(HttpHeaders.COOKIE, String.format("%s=%s", cookie.getName(), cookie.getValue()))); + for (RequestCookie cookie : request.getCookies()) { + headers.add(header(HttpHeaders.COOKIE, + String.format("%s=%s", cookie.getName(), cookie.getValue()))); } if (requiresFormEncodingContentTypeHeader(request)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 93972dba0..f768663dd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -19,8 +19,6 @@ import java.net.URI; import java.util.Collection; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -89,12 +87,12 @@ public interface OperationRequest { URI getUri(); /** - * Returns {@link Cookie Cookies} sent with the request. If no cookies were sent an - * empty collection is returned. + * Returns the {@link RequestCookie cookies} sent with the request. If no cookies were + * sent an empty collection is returned. * * @return the cookies, never {@code null} * @since 1.2.0 */ - Collection getCookies(); + Collection getCookies(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 3f75e289b..da1c07cf5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -20,8 +20,6 @@ import java.util.Collection; import java.util.Collections; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -48,8 +46,7 @@ public class OperationRequestFactory { */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, - Collection cookies) { + Collection parts, Collection cookies) { return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, uri, content), parameters, parts, cookies); } @@ -70,7 +67,8 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { - return create(uri, method, content, headers, parameters, parts, Collections.emptyList()); + return create(uri, method, content, headers, parameters, parts, + Collections.emptyList()); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java new file mode 100644 index 000000000..595a19f22 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2017 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.operation; + +/** + * A representation of a Cookie received in a request. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public final class RequestCookie { + + private final String name; + + private final String value; + + /** + * Creates a new {@code RequestCookie} with the given {@code name} and {@code value}. + * + * @param name the name of the cookie + * @param value the value of the cookie + */ + public RequestCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of the cookie. + * + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the cookie. + * + * @return the value + */ + public String getValue() { + return this.value; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 8495c6c99..5fb575088 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -20,8 +20,6 @@ import java.util.Collection; import java.util.Collections; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,7 +39,7 @@ class StandardOperationRequest extends AbstractOperationMessage private URI uri; - private Collection cookies; + private Collection cookies; /** * Creates a new request with the given {@code uri} and {@code method}. The request @@ -58,7 +56,7 @@ class StandardOperationRequest extends AbstractOperationMessage */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, Collection cookies) { + Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; @@ -88,7 +86,7 @@ public URI getUri() { } @Override - public Collection getCookies() { + public Collection getCookies() { return this.cookies; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 6d210b5f1..032b0845f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -25,8 +25,6 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.Cookie; - import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; @@ -156,7 +155,7 @@ public final class OperationRequestBuilder { private List partBuilders = new ArrayList<>(); - private Collection cookies = new ArrayList<>(); + private Collection cookies = new ArrayList<>(); private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); @@ -215,7 +214,7 @@ public OperationRequestPartBuilder part(String name, byte[] content) { } public OperationRequestBuilder cookie(String name, String value) { - this.cookies.add(new Cookie(name, value)); + this.cookies.add(new RequestCookie(name, value)); return this; } diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index d8a0529cb..5baca9c25 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,6 +1,7 @@ description = 'Spring REST Docs MockMvc' dependencies { + compile 'javax.servlet:javax.servlet-api' compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') optional 'junit:junit' diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 2aeb6119a..af66e4017 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -21,14 +21,12 @@ import java.io.StringWriter; import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map.Entry; import javax.servlet.ServletException; -import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -71,7 +70,7 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); - Collection cookies = extractCookies(mockRequest); + Collection cookies = extractCookies(mockRequest); String queryString = mockRequest.getQueryString(); if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { @@ -90,12 +89,16 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { } } - private Collection extractCookies(MockHttpServletRequest mockRequest) { - if (mockRequest.getCookies() != null) { - return Arrays.asList(mockRequest.getCookies()); + private Collection extractCookies(MockHttpServletRequest mockRequest) { + if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + return Collections.emptyList(); } - - return Collections.emptyList(); + List cookies = new ArrayList<>(); + for (javax.servlet.http.Cookie servletCookie : mockRequest.getCookies()) { + cookies.add( + new RequestCookie(servletCookie.getName(), servletCookie.getValue())); + } + return cookies; } private List extractParts(MockHttpServletRequest servletRequest) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index d2a18373c..8b6c67eba 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.Iterator; -import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.junit.Test; @@ -33,6 +32,7 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -92,20 +92,22 @@ public void requestWithHeaders() throws Exception { @Test public void requestWithCookies() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo").cookie(new Cookie("cookieName1", "cookieVal1"), - new Cookie("cookieName2", "cookieVal2"))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo") + .cookie(new javax.servlet.http.Cookie("cookieName1", + "cookieVal1"), + new javax.servlet.http.Cookie("cookieName2", "cookieVal2"))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); assertThat(request.getCookies().size(), is(equalTo(2))); - Iterator cookieIterator = request.getCookies().iterator(); + Iterator cookieIterator = request.getCookies().iterator(); - Cookie cookie1 = cookieIterator.next(); + RequestCookie cookie1 = cookieIterator.next(); assertThat(cookie1.getName(), is(equalTo("cookieName1"))); assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); - Cookie cookie2 = cookieIterator.next(); + RequestCookie cookie2 = cookieIterator.next(); assertThat(cookie2.getName(), is(equalTo("cookieName2"))); assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 6b64c6a3c..852db3a26 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -39,6 +39,7 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; @@ -60,12 +61,11 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { extractCookies(requestSpec)); } - private Collection extractCookies( + private Collection extractCookies( FilterableRequestSpecification requestSpec) { - Collection cookies = new ArrayList<>(); + Collection cookies = new ArrayList<>(); for (Cookie cookie : requestSpec.getCookies()) { - cookies.add( - new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + cookies.add(new RequestCookie(cookie.getName(), cookie.getValue())); } return cookies; } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 4617cb30d..906b8647b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -25,8 +25,6 @@ import java.util.Collection; import java.util.Iterator; -import javax.servlet.http.Cookie; - import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; @@ -44,6 +42,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -154,13 +153,13 @@ public void cookies() { .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getCookies().size(), is(equalTo(2))); - Iterator cookieIterator = request.getCookies().iterator(); - Cookie cookie1 = cookieIterator.next(); + Iterator cookieIterator = request.getCookies().iterator(); + RequestCookie cookie1 = cookieIterator.next(); assertThat(cookie1.getName(), is(equalTo("cookie1"))); assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); - Cookie cookie2 = cookieIterator.next(); + RequestCookie cookie2 = cookieIterator.next(); assertThat(cookie2.getName(), is(equalTo("cookie2"))); assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); } From 31ad4b40a066d81c0526622b0cb9996ae68be399 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Mar 2017 16:57:55 +0000 Subject: [PATCH 0367/1059] Upgrade the Grails sample to Grails 3.2.6 Closes gh-357 --- samples/rest-notes-grails/build.gradle | 43 ++++++++--------- samples/rest-notes-grails/gradle.properties | 4 +- .../grails-app/conf/application.yml | 2 +- .../com/example/IndexController.groovy | 48 ------------------- .../InternalServerErrorController.groovy | 28 ----------- .../com/example/NotFoundController.groovy | 28 ----------- .../{ => com/example}/UrlMappings.groovy | 16 +++++-- .../init/{ => com/example}/BootStrap.groovy | 2 +- .../grails-app/views/error.gson | 6 +++ .../grails-app/views/notFound.gson | 6 +++ .../src/docs/asciidoc/index.adoc | 24 ---------- .../com/example/ApiDocumentationSpec.groovy | 33 +------------ 12 files changed, 48 insertions(+), 192 deletions(-) delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy rename samples/rest-notes-grails/grails-app/controllers/{ => com/example}/UrlMappings.groovy (58%) rename samples/rest-notes-grails/grails-app/init/{ => com/example}/BootStrap.groovy (97%) create mode 100644 samples/rest-notes-grails/grails-app/views/error.gson create mode 100644 samples/rest-notes-grails/grails-app/views/notFound.gson diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 64e2e6d8e..c0bce855d 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -1,36 +1,26 @@ buildscript { - ext { - grailsVersion = project.grailsVersion - } repositories { - mavenLocal() maven { url "https://repo.grails.org/grails/core" } - maven { url 'https://repo.spring.io/libs-snapshot' } } dependencies { classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate:4.3.10.5" - classpath 'org.ajoberstar:gradle-git:1.1.0' + classpath "org.grails.plugins:hibernate5:6.0.7" } } plugins { - id "io.spring.dependency-management" version "0.5.4.RELEASE" id 'org.asciidoctor.convert' version '1.5.3' } version "0.1" group "com.example" -apply plugin: "spring-boot" apply plugin: "war" -apply plugin: 'eclipse' -apply plugin: 'idea' +apply plugin: "eclipse" +apply plugin: "idea" apply plugin: "org.grails.grails-web" ext { - grailsVersion = project.grailsVersion - gradleWrapperVersion = project.gradleWrapperVersion restDocsVersion = "1.2.0.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } @@ -43,12 +33,12 @@ repositories { dependencyManagement { dependencies { + dependency "org.springframework.restdocs:spring-restdocs-core:$restDocsVersion" dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" dependency "org.springframework.restdocs:spring-restdocs-asciidoctor:$restDocsVersion" } imports { mavenBom "org.grails:grails-bom:$grailsVersion" - mavenBom "org.springframework:spring-framework-bom:4.3.1.RELEASE" } applyMavenExclusions false } @@ -57,11 +47,13 @@ dependencies { asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor" compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-core" compile "org.grails:grails-plugin-url-mappings" compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-codecs" compile "org.grails:grails-plugin-interceptors" compile "org.grails:grails-plugin-services" compile "org.grails:grails-plugin-datasource" @@ -69,22 +61,25 @@ dependencies { compile "org.grails:grails-plugin-async" compile "org.grails:grails-web-boot" compile "org.grails:grails-logging" - - compile "org.grails.plugins:hibernate" compile "org.grails.plugins:cache" - compile "org.hibernate:hibernate-ehcache" + compile "org.grails.plugins:hibernate5" + compile "org.hibernate:hibernate-core:5.1.2.Final" + compile "org.hibernate:hibernate-ehcache:5.1.2.Final" + compile "org.grails.plugins:views-json" + compile "org.grails.plugins:views-json-templates" + console "org.grails:grails-console" + + profile "org.grails.profiles:rest-api" runtime "com.h2database:h2" testCompile "org.grails:grails-plugin-testing" testCompile "org.grails.plugins:geb" - testCompile 'org.springframework.restdocs:spring-restdocs-restassured' - - console "org.grails:grails-console" -} + testCompile "org.grails:grails-datastore-rest-client" + testCompile "org.springframework.restdocs:spring-restdocs-restassured" -task wrapper(type: Wrapper) { - gradleVersion = gradleWrapperVersion + testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" + testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" } ext { diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties index 1b1c50f8e..d8bda96d7 100644 --- a/samples/rest-notes-grails/gradle.properties +++ b/samples/rest-notes-grails/gradle.properties @@ -1,2 +1,2 @@ -grailsVersion=3.0.15 -gradleWrapperVersion=2.3 +grailsVersion=3.2.6 +gradleWrapperVersion=3.4 \ No newline at end of file diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml index 3fe5b2978..915c149a9 100644 --- a/samples/rest-notes-grails/grails-app/conf/application.yml +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -1,6 +1,6 @@ --- grails: - profile: web-api + profile: rest-api codegen: defaultPackage: com.example info: diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy deleted file mode 100644 index 01f585182..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -import grails.core.GrailsApplication -import grails.util.Environment - -class IndexController { - - GrailsApplication grailsApplication - - def index() { - render(contentType: 'application/json') { - message = "Welcome to Grails!" - environment = Environment.current.name - appversion = grailsApplication.metadata['info.app.version'] - grailsversion = grailsApplication.metadata['info.app.grailsVersion'] - appprofile = grailsApplication.config.grails?.profile - groovyversion = GroovySystem.getVersion() - jvmversion = System.getProperty('java.version') - controllers = array { - for (c in grailsApplication.controllerClasses) { - controller([name: c.fullName]) - } - } - plugins = array { - for (p in grailsApplication.mainContext.pluginManager.allPlugins) { - plugin([name: p.fullName]) - } - } - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy deleted file mode 100644 index c0e80312c..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -class InternalServerErrorController { - - def index() { - render(contentType: 'application/json') { - error = 500 - message = "Internal server error" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy deleted file mode 100644 index 039968a93..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -class NotFoundController { - - def index() { - render(contentType: 'application/json') { - error = 404 - message = "Not Found" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy similarity index 58% rename from samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy rename to samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy index 7d8cffe56..190e41e39 100644 --- a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -14,12 +14,20 @@ * limitations under the License. */ +package com.example + class UrlMappings { static mappings = { - "/"(controller: 'index') - "500"(controller: 'InternalServerError') - "404"(controller: 'NotFound') + delete "/$controller/$id(.$format)?"(action:"delete") + get "/$controller(.$format)?"(action:"index") + get "/$controller/$id(.$format)?"(action:"show") + post "/$controller(.$format)?"(action:"save") + put "/$controller/$id(.$format)?"(action:"update") + patch "/$controller/$id(.$format)?"(action:"patch") + + "500"(view: '/error') + "404"(view: '/notFound') } } diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy similarity index 97% rename from samples/rest-notes-grails/grails-app/init/BootStrap.groovy rename to samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy index 0efbc954f..92ec4c144 100644 --- a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy +++ b/samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy @@ -14,7 +14,7 @@ * limitations under the License. */ -import com.example.Note +package com.example class BootStrap { diff --git a/samples/rest-notes-grails/grails-app/views/error.gson b/samples/rest-notes-grails/grails-app/views/error.gson new file mode 100644 index 000000000..314e80db7 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/views/error.gson @@ -0,0 +1,6 @@ +response.status 500 + +json { + message "Internal server error" + error 500 +} \ No newline at end of file diff --git a/samples/rest-notes-grails/grails-app/views/notFound.gson b/samples/rest-notes-grails/grails-app/views/notFound.gson new file mode 100644 index 000000000..1a710b267 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/views/notFound.gson @@ -0,0 +1,6 @@ +response.status 404 + +json { + message "Not Found" + error 404 +} \ No newline at end of file diff --git a/samples/rest-notes-grails/src/docs/asciidoc/index.adoc b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc index 231af2ecf..c1dd35966 100644 --- a/samples/rest-notes-grails/src/docs/asciidoc/index.adoc +++ b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc @@ -62,30 +62,6 @@ use of HTTP status codes. = Resources -[[resources-index]] -== Index - -The index provides the entry point into the service. - - - -[[resources-index-access]] -=== Accessing the index - -A `GET` request is used to access the index - -==== Example request - -include::{snippets}/index-example/curl-request.adoc[] - -==== Response structure - -include::{snippets}/index-example/response-fields.adoc[] - -==== Example response - -include::{snippets}/index-example/http-response.adoc[] - [[resources-notes]] == Notes diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 102426b96..2e15d73bc 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -59,34 +59,6 @@ class ApiDocumentationSpec extends Specification { .build() } - void 'test and document get request for /index'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('index-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('message').description('Welcome to Grails!'), - fieldWithPath('environment').description("The running environment"), - fieldWithPath('appversion').description('version of the app that is running'), - fieldWithPath('grailsversion').description('the version of grails used in this project'), - fieldWithPath('appprofile').description('the profile of grails used in this project'), - fieldWithPath('groovyversion').description('the version of groovy used in this project'), - fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - subsectionWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - subsectionWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), - ))) - .when() - .port(this.serverPort) - .get('/') - .then() - .assertThat() - .statusCode(is(200)) - } - void 'test and document notes list request'() { expect: given(this.documentationSpec) @@ -97,7 +69,6 @@ class ApiDocumentationSpec extends Specification { .removePort()), preprocessResponse(prettyPrint()), responseFields( - fieldWithPath('[].class').description('the class of the resource'), fieldWithPath('[].id').description('the id of the note'), fieldWithPath('[].title').description('the title of the note'), fieldWithPath('[].body').description('the body of the note'), @@ -127,7 +98,6 @@ class ApiDocumentationSpec extends Specification { subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') ), responseFields( - fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), @@ -152,7 +122,6 @@ class ApiDocumentationSpec extends Specification { .removePort()), preprocessResponse(prettyPrint()), responseFields( - fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), From b6f986556e8ab19ccfdb984381687b294ba62ea1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Mar 2017 21:03:39 +0000 Subject: [PATCH 0368/1059] Upgrade to Gradle 3.4 Closes gh-283 --- build.gradle | 8 ++++++-- .../build/SampleBuildConfigurer.groovy | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index b4f341154..b18f28f6e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,11 +5,15 @@ buildscript { maven { url 'https://plugins.gradle.org/m2/' } } dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.5.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - classpath 'io.spring.gradle:spring-io-plugin:0.0.5.RELEASE' + classpath 'io.spring.gradle:spring-io-plugin:0.0.6.RELEASE' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:1.2' } + configurations.classpath.resolutionStrategy.eachDependency { + if (it.requested.name == 'dependency-management-plugin') { + it.useVersion '1.0.0.RELEASE' + } + } } allprojects { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 69b6f1fd5..5ef87faed 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -121,7 +121,7 @@ public class SampleBuildConfigurer { private Task createVerifyIncludes(Project project, File buildDir) { Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") verifyIncludesTask.description = "Verifies the includes in the ${name} sample" - verifyIncludesTask << { + verifyIncludesTask.doLast { Map unprocessedIncludes = [:] buildDir.eachFileRecurse { file -> if (file.name.endsWith('.html')) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Sat, 4 Mar 2017 20:40:42 +0000 Subject: [PATCH 0369/1059] Remove use of Spring Boot from REST Assured module's integration tests Closes gh-360 --- spring-restdocs-restassured/build.gradle | 5 +- .../RestAssuredRequestConverterTests.java | 86 +++++--------- ...uredRestDocumentationIntegrationTests.java | 108 ++++++------------ .../restdocs/restassured/TomcatServer.java | 107 +++++++++++++++++ 4 files changed, 171 insertions(+), 135 deletions(-) create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index a861c7e51..245a697ba 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -9,13 +9,10 @@ dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' + testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.11' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' - testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') - testRuntime 'commons-logging:commons-logging:1.2' } test { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 9c4f7615e..9b04d0786 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -28,25 +28,15 @@ import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -57,33 +47,29 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") public class RestAssuredRequestConverterTests { + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); + @Rule public final ExpectedException thrown = ExpectedException.none(); private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); - @Value("${local.server.port}") - private int port; - @Test public void requestUri() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/foo/bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getUri(), - is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); + assertThat(request.getUri(), is(equalTo( + URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")))); } @Test public void requestMethod() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.head("/foo/bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -92,7 +78,7 @@ public void requestMethod() { @Test public void queryStringParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -103,7 +89,7 @@ public void queryStringParameters() { @Test public void queryStringFromUrlParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/?foo=bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -113,7 +99,7 @@ public void queryStringFromUrlParameters() { @Test public void formParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .formParameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -124,7 +110,7 @@ public void formParameters() { @Test public void requestParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .parameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -135,7 +121,7 @@ public void requestParameters() { @Test public void headers() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -144,15 +130,15 @@ public void headers() { assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); assertThat(request.getHeaders().get("Host"), - is(equalTo(Arrays.asList("localhost:" + this.port)))); + is(equalTo(Arrays.asList("localhost:" + tomcat.getPort())))); } @Test public void multipart() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("a", "a.txt", "alpha", null) .multiPart("b", new ObjectBody("bar"), "application/json"); - requestSpec.post().then().statusCode(200); + requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); Collection parts = request.getParts(); @@ -174,7 +160,7 @@ public void multipart() { @Test public void byteArrayBody() { RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); this.factory.convert((FilterableRequestSpecification) requestSpec); } @@ -182,7 +168,7 @@ public void byteArrayBody() { @Test public void stringBody() { RequestSpecification requestSpec = RestAssured.given().body("body") - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -192,7 +178,7 @@ public void stringBody() { @Test public void objectBody() { RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -203,7 +189,7 @@ public void objectBody() { public void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -213,7 +199,7 @@ public void byteArrayInputStreamBody() { @Test public void fileBody() { RequestSpecification requestSpec = RestAssured.given() - .body(new File("src/test/resources/body.txt")).port(this.port); + .body(new File("src/test/resources/body.txt")).port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -224,7 +210,7 @@ public void fileBody() { public void fileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); RequestSpecification requestSpec = RestAssured.given().body(inputStream) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Cannot read content from input stream " + inputStream @@ -234,7 +220,7 @@ public void fileInputStreamBody() throws FileNotFoundException { @Test public void multipartWithByteArrayInputStreamBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); requestSpec.post(); OperationRequest request = this.factory @@ -245,7 +231,7 @@ public void multipartWithByteArrayInputStreamBody() { @Test public void multipartWithStringBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", "foo"); requestSpec.post(); OperationRequest request = this.factory @@ -256,7 +242,7 @@ public void multipartWithStringBody() { @Test public void multipartWithByteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", "file", "foo".getBytes()); requestSpec.post(); OperationRequest request = this.factory @@ -267,7 +253,7 @@ public void multipartWithByteArrayBody() { @Test public void multipartWithFileBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart(new File("src/test/resources/body.txt")); requestSpec.post(); OperationRequest request = this.factory @@ -279,7 +265,7 @@ public void multipartWithFileBody() { @Test public void multipartWithFileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("foo", "foo.txt", inputStream); requestSpec.post(); this.thrown.expect(IllegalStateException.class); @@ -290,7 +276,7 @@ public void multipartWithFileInputStreamBody() throws FileNotFoundException { @Test public void multipartWithObjectBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", new ObjectBody("bar")); requestSpec.post(); OperationRequest request = this.factory @@ -299,20 +285,6 @@ public void multipartWithObjectBody() { is(equalTo("{\"foo\":\"bar\"}"))); } - /** - * Minimal test web application. - */ - @Configuration - @EnableAutoConfiguration - @RestController - static class TestApplication { - - @RequestMapping("/") - void handle() { - } - - } - /** * Sample object body to verify JSON serialization. */ diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index f81c91f39..4d1c4072f 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -19,33 +19,19 @@ import java.io.File; import java.net.URL; import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Pattern; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; import static com.jayway.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; @@ -84,22 +70,19 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") public class RestAssuredRestDocumentationIntegrationTests { @Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); - @Value("${local.server.port}") - private int port; + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); @Test public void defaultSnippetGeneration() { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("default")).get("/").then().statusCode(200); assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); @@ -108,7 +91,8 @@ public void defaultSnippetGeneration() { @Test public void curlSnippetWithContent() throws Exception { String contentType = "text/plain; charset=UTF-8"; - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-content")).accept("application/json") .content("content").contentType(contentType).post("/").then() .statusCode(200); @@ -117,7 +101,7 @@ public void curlSnippetWithContent() throws Exception { new File( "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl 'http://localhost:" + this.port + "/' -i " + .content("$ curl 'http://localhost:" + tomcat.getPort() + "/' -i " + "-X POST -H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " + "-d 'content'")))); @@ -125,7 +109,8 @@ public void curlSnippetWithContent() throws Exception { @Test public void curlSnippetWithQueryStringOnPost() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-query-string")) .accept("application/json").param("foo", "bar").param("a", "alpha") .post("/?foo=bar").then().statusCode(200); @@ -134,7 +119,7 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl " + "'http://localhost:" + this.port + .content("$ curl " + "'http://localhost:" + tomcat.getPort() + "/?foo=bar' -i -X POST " + "-H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " + "-d 'a=alpha'")))); @@ -142,7 +127,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { @Test public void linksSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("links", links(linkWithRel("rel").description("The description")))) .accept("application/json").get("/").then().statusCode(200); @@ -153,7 +139,8 @@ public void linksSnippet() throws Exception { @Test public void pathParametersSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("path-parameters", pathParameters( parameterWithName("foo").description("The description")))) @@ -165,7 +152,8 @@ public void pathParametersSnippet() throws Exception { @Test public void requestParametersSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-parameters", requestParameters( parameterWithName("foo").description("The description")))) @@ -179,7 +167,8 @@ public void requestParametersSnippet() throws Exception { @Test public void requestFieldsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) .accept("application/json").content("{\"a\":\"alpha\"}").post("/").then() @@ -191,7 +180,8 @@ public void requestFieldsSnippet() throws Exception { @Test public void requestPartsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) .multiPart("a", "foo").post("/upload").then().statusCode(200); @@ -202,7 +192,8 @@ public void requestPartsSnippet() throws Exception { @Test public void responseFieldsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("response-fields", responseFields(fieldWithPath("a").description("The description"), fieldWithPath("links") @@ -216,7 +207,8 @@ public void responseFieldsSnippet() throws Exception { @Test public void parameterizedOutputDirectory() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("{method-name}")).get("/").then().statusCode(200); assertExpectedSnippetFilesExist( new File("build/generated-snippets/parameterized-output-directory"), @@ -225,7 +217,7 @@ public void parameterizedOutputDirectory() throws Exception { @Test public void multiStep() throws Exception { - RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) .addFilter(documentationConfiguration(this.restDocumentation)) .addFilter(document("{method-name}-{step}")).build(); given(spec).get("/").then().statusCode(200); @@ -245,7 +237,7 @@ public void multiStep() throws Exception { @Test public void additionalSnippets() throws Exception { RestDocumentationFilter documentation = document("{method-name}-{step}"); - RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) .addFilter(documentationConfiguration(this.restDocumentation)) .addFilter(documentation).build(); given(spec) @@ -262,7 +254,8 @@ public void additionalSnippets() throws Exception { @Test public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .header("a", "alpha").header("b", "bravo").contentType("application/json") .accept("application/json").content("{\"a\":\"alpha\"}") .filter(document("original-request")) @@ -279,7 +272,7 @@ public void preprocessedRequest() throws Exception { .header("a", "alpha").header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost:" + this.port) + .header("Host", "localhost:" + tomcat.getPort()) .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); @@ -297,7 +290,8 @@ public void preprocessedRequest() throws Exception { @Test public void preprocessedResponse() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("original-response")) .filter(document("preprocessed-response", preprocessResponse( prettyPrint(), maskLinks(), @@ -327,7 +321,7 @@ public void customSnippetTemplate() throws Exception { ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { - given().port(this.port).accept("application/json") + given().port(tomcat.getPort()).accept("application/json") .filter(documentationConfiguration(this.restDocumentation)) .filter(document("custom-snippet-template")).get("/").then() .statusCode(200); @@ -348,38 +342,4 @@ private void assertExpectedSnippetFilesExist(File directory, String... snippets) } } - /** - * Minimal test application called by the tests. - */ - @Configuration - @EnableAutoConfiguration - @RestController - static class TestApplication { - - @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> foo() { - Map response = new HashMap<>(); - response.put("a", "alpha"); - Map link = new HashMap<>(); - link.put("rel", "rel"); - link.put("href", "href"); - response.put("links", Arrays.asList(link)); - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "alpha"); - headers.add("Foo", "http://localhost:12345/foo/bar"); - return new ResponseEntity<>(response, headers, HttpStatus.OK); - } - - @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) - public String bar() { - return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; - } - - @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public void upload() { - - } - - } - } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java new file mode 100644 index 000000000..75ee97663 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2014-2017 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.restassured; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.rules.ExternalResource; + +/** + * {@link ExternalResource} that starts and stops a Tomcat server. + * + * @author Andy Wilkinson + */ +class TomcatServer extends ExternalResource { + + private Tomcat tomcat; + + private int port; + + @Override + protected void before() throws LifecycleException { + this.tomcat = new Tomcat(); + this.tomcat.getConnector().setPort(0); + Context context = this.tomcat.addContext("/", null); + this.tomcat.addServlet("/", "test", new TestServlet()); + context.addServletMappingDecoded("/", "test"); + this.tomcat.start(); + this.port = this.tomcat.getConnector().getLocalPort(); + } + + @Override + protected void after() { + try { + this.tomcat.stop(); + } + catch (LifecycleException ex) { + throw new RuntimeException(ex); + } + } + + int getPort() { + return this.port; + } + + /** + * {@link HttpServlet} used to handle requests in the tests. + */ + private static final class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + private void respondWithJson(HttpServletResponse response) + throws IOException, JsonProcessingException { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + Map content = new HashMap<>(); + content.put("a", "alpha"); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + content.put("links", Arrays.asList(link)); + response.getWriter().println(new ObjectMapper().writeValueAsString(content)); + response.setHeader("a", "alpha"); + response.setHeader("Foo", "http://localhost:12345/foo/bar"); + response.flushBuffer(); + } + + } + +} From 727f6ccd78a63870d643d3ae49b50ffeb8b4d940 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 20:45:47 +0000 Subject: [PATCH 0370/1059] Protect the tests against changes to MockMvc's HTTP header ordering Closes gh-359 --- ...kMvcRestDocumentationIntegrationTests.java | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 2b2df2e8e..edc4c3331 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -21,6 +21,7 @@ import java.net.URLClassLoader; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -39,10 +40,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; +import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.FileSystemUtils; import org.springframework.web.bind.annotation.RequestMapping; @@ -60,6 +63,7 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -317,8 +321,9 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", responseFields( - fieldWithPath("a").description("The description"), + .andDo(document("links", + responseFields(fieldWithPath("a") + .description("The description"), fieldWithPath("links").description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), @@ -388,38 +393,44 @@ public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - mockMvc.perform(get("/").header("a", "alpha").header("b", "bravo") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) + MvcResult result = mockMvc + .perform(get("/").header("a", "alpha").header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) .andExpect(status().isOk()).andDo(document("original-request")) .andDo(document("preprocessed-request", preprocessRequest(prettyPrint(), removeHeaders("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH), - replacePattern(pattern, "\"<>\"")))); + replacePattern(pattern, "\"<>\"")))) + .andReturn(); + HttpRequestMatcher originalRequest = httpRequest(asciidoctor(), RequestMethod.GET, + "/"); + for (String headerName : iterable(result.getRequest().getHeaderNames())) { + originalRequest.header(headerName, result.getRequest().getHeader(headerName)); + } assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("a", "alpha").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost:8080") - .header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); + is(snippet(asciidoctor()).withContents(originalRequest + .header("Host", "localhost:8080").header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); + HttpRequestMatcher preprocessedRequest = httpRequest(asciidoctor(), + RequestMethod.GET, "/"); + List removedHeaders = Arrays.asList("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH); + for (String headerName : iterable(result.getRequest().getHeaderNames())) { + if (!removedHeaders.contains(headerName)) { + preprocessedRequest.header(headerName, + result.getRequest().getHeader(headerName)); + } + } String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + .withContents(preprocessedRequest.content(prettyPrinted)))); } @Test From 3a0f4ab68e0bc23e18212c38c169e8f5f0492904 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 21:43:12 +0000 Subject: [PATCH 0371/1059] Fix the Slate sample's tests Closes gh-339 --- samples/rest-notes-slate/build.gradle | 1 - .../com/example/notes/ApiDocumentation.java | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d65d9d483..2911c2884 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,7 +27,6 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' -ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 073af2bcd..bc611323d 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -28,6 +28,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.templates.TemplateFormats.markdown; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -116,7 +117,7 @@ public void indexExample() throws Exception { linkWithRel("tags").description("The [Tags](#tags) resource"), linkWithRel("profile").description("The ALPS profile for the service")), responseFields( - fieldWithPath("_links").description("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @@ -134,7 +135,8 @@ public void notesListExample() throws Exception { .andExpect(status().isOk()) .andDo(document("notes-list-example", responseFields( - fieldWithPath("_embedded.notes").description("An array of [Note](#note) resources")))); + subsectionWithPath("_embedded.notes").description("An array of [Note](#note) resources"), + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -197,12 +199,13 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("_links.tags", is(notNullValue()))) .andDo(document("note-get-example", links( - linkWithRel("self").description("This note"), + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("note").description("This note"), linkWithRel("tags").description("This note's tags")), responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -218,7 +221,8 @@ public void tagsListExample() throws Exception { .andExpect(status().isOk()) .andDo(document("tags-list-example", responseFields( - fieldWithPath("_embedded.tags").description("An array of [Tag](#tag) resources")))); + subsectionWithPath("_embedded.tags").description("An array of [Tag](#tag) resources"), + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -295,11 +299,12 @@ public void tagGetExample() throws Exception { .andExpect(jsonPath("name", is(tag.get("name")))) .andDo(document("tag-get-example", links( - linkWithRel("self").description("This tag"), + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("tag").description("This tag"), linkWithRel("notes").description("The notes that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @Test From ca86e3ad8c874fce6211876daf3ce7db9b9f1ace Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 22:12:09 +0000 Subject: [PATCH 0372/1059] Upgrade Slate sample to Slate 1.5 Closes gh-361 --- samples/rest-notes-slate/slate/.gitignore | 4 +- samples/rest-notes-slate/slate/CHANGELOG.md | 50 +- samples/rest-notes-slate/slate/Gemfile | 15 +- samples/rest-notes-slate/slate/Gemfile.lock | 194 +- samples/rest-notes-slate/slate/README.md | 119 +- samples/rest-notes-slate/slate/Rakefile | 6 - samples/rest-notes-slate/slate/Vagrantfile | 39 + samples/rest-notes-slate/slate/config.rb | 11 + samples/rest-notes-slate/slate/deploy.sh | 203 + .../rest-notes-slate/slate/lib/multilang.rb | 12 + ...api-guide.md.erb => api-guide.html.md.erb} | 0 .../slate/source/images/logo.png | Bin 3507 -> 22317 bytes .../slate/source/includes/_errors.md | 20 + .../slate/source/index.html.md | 189 + .../slate/source/javascripts/app/_lang.js | 8 +- .../slate/source/javascripts/app/_search.js | 1 + .../slate/source/javascripts/app/_toc.js | 2 + .../slate/source/javascripts/lib/_jquery.js | 9831 +++++++++++++++++ .../slate/source/layouts/layout.erb | 20 +- .../{_normalize.css => _normalize.scss} | 0 .../slate/source/stylesheets/_syntax.scss.erb | 27 - .../slate/source/stylesheets/_variables.scss | 84 +- .../slate/source/stylesheets/print.css.scss | 7 +- .../slate/source/stylesheets/screen.css.scss | 25 +- 24 files changed, 10580 insertions(+), 287 deletions(-) delete mode 100644 samples/rest-notes-slate/slate/Rakefile create mode 100644 samples/rest-notes-slate/slate/Vagrantfile create mode 100755 samples/rest-notes-slate/slate/deploy.sh create mode 100644 samples/rest-notes-slate/slate/lib/multilang.rb rename samples/rest-notes-slate/slate/source/{api-guide.md.erb => api-guide.html.md.erb} (100%) create mode 100644 samples/rest-notes-slate/slate/source/includes/_errors.md create mode 100644 samples/rest-notes-slate/slate/source/index.html.md create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js rename samples/rest-notes-slate/slate/source/stylesheets/{_normalize.css => _normalize.scss} (100%) delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore index f6fc8c00b..105015835 100644 --- a/samples/rest-notes-slate/slate/.gitignore +++ b/samples/rest-notes-slate/slate/.gitignore @@ -14,9 +14,11 @@ tmp *.DS_STORE build/ .cache +.vagrant +.sass-cache # YARD artifacts .yardoc _yardoc doc/ -.idea/ \ No newline at end of file +.idea/ diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md index ee496db6c..be29842dc 100644 --- a/samples/rest-notes-slate/slate/CHANGELOG.md +++ b/samples/rest-notes-slate/slate/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## Version 1.5.0 + +*February 23, 2017* + +- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature +- Upgrade Middleman to add Ruby 1.4.0 compatibility +- Switch default code highlighting color scheme to better highlight JSON +- Various small typo and bug fixes + +## Version 1.4.0 + +*November 24, 2016* + +- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs +- Update some links in README +- Fix broken Vagrant startup script +- Fix some problems with deploy.sh help message +- Fix bug with language tabs not hiding properly if no error +- Add `!default` to SASS variables +- Fix bug with logo margin +- Bump tested Ruby versions in .travis.yml + +## Version 1.3.3 + +*June 11, 2016* + +Documentation and example changes. + +## Version 1.3.2 + +*February 3, 2016* + +A small bugfix for slightly incorrect background colors on code samples in some cases. + +## Version 1.3.1 + +*January 31, 2016* + +A small bugfix for incorrect whitespace in code blocks. + +## Version 1.3 + +*January 27, 2016* + +We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs. + +Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages. + ## Version 1.2 *June 20, 2015* @@ -21,7 +69,7 @@ ## Version 1.1 -*July 27th, 2014* +*July 27, 2014* **Fixes:** diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile index 0933b9d68..1bff87424 100644 --- a/samples/rest-notes-slate/slate/Gemfile +++ b/samples/rest-notes-slate/slate/Gemfile @@ -1,12 +1,9 @@ source 'https://rubygems.org' # Middleman -gem 'middleman', '~>3.3.10' -gem 'middleman-gh-pages', '~> 0.0.3' -gem 'middleman-syntax', '~> 2.0.0' -gem 'middleman-autoprefixer', '~> 2.4.4' -gem 'rouge', '~> 1.9.0' -gem 'redcarpet', '~> 3.3.2' - -gem 'rake', '~> 10.4.2' -gem 'therubyracer', '~> 0.12.1', platforms: :ruby +gem 'middleman', '~>4.2.1' +gem 'middleman-syntax', '~> 3.0.0' +gem 'middleman-autoprefixer', '~> 2.7.0' +gem "middleman-sprockets", "~> 4.1.0" +gem 'rouge', '~> 2.0.5' +gem 'redcarpet', '~> 3.4.0' diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock index fff5ee10c..adacad9a8 100644 --- a/samples/rest-notes-slate/slate/Gemfile.lock +++ b/samples/rest-notes-slate/slate/Gemfile.lock @@ -1,140 +1,122 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.1.11) - i18n (~> 0.6, >= 0.6.9) - json (~> 1.7, >= 1.7.7) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) minitest (~> 5.1) - thread_safe (~> 0.1) tzinfo (~> 1.1) - autoprefixer-rails (5.2.0.1) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + autoprefixer-rails (6.6.1) execjs - json - celluloid (0.16.0) - timers (~> 4.0.0) - chunky_png (1.3.4) + backports (3.6.8) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) - compass (1.0.3) - chunky_png (~> 1.2) - compass-core (~> 1.0.2) - compass-import-once (~> 1.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - sass (>= 3.3.13, < 3.5) - compass-core (1.0.3) - multi_json (~> 1.0) - sass (>= 3.3.0, < 3.5) + coffee-script-source (1.12.2) compass-import-once (1.0.5) sass (>= 3.2, < 3.5) + concurrent-ruby (1.0.4) + contracts (0.13.0) + dotenv (2.2.0) erubis (2.7.0) - execjs (2.5.2) - ffi (1.9.8) - haml (4.0.6) + execjs (2.7.0) + fast_blank (1.0.0) + fastimage (2.0.1) + addressable (~> 2) + ffi (1.9.17) + haml (4.0.7) tilt - hike (1.2.3) - hitimes (1.2.2) - hooks (0.4.0) - uber (~> 0.0.4) + hamster (3.0.0) + concurrent-ruby (~> 1.0) + hashie (3.5.1) i18n (0.7.0) - json (1.8.3) - kramdown (1.7.0) - libv8 (3.16.14.7) - listen (2.10.1) - celluloid (~> 0.16.0) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - middleman (3.3.12) + kramdown (1.13.2) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + memoist (0.15.0) + middleman (4.2.1) coffee-script (~> 2.2) - compass (>= 1.0.0, < 2.0.0) compass-import-once (= 1.0.5) - execjs (~> 2.0) haml (>= 4.0.5) kramdown (~> 1.2) - middleman-core (= 3.3.12) - middleman-sprockets (>= 3.1.2) + middleman-cli (= 4.2.1) + middleman-core (= 4.2.1) sass (>= 3.4.0, < 4.0) - uglifier (~> 2.5) - middleman-autoprefixer (2.4.4) - autoprefixer-rails (~> 5.2.0) + middleman-autoprefixer (2.7.1) + autoprefixer-rails (>= 6.5.2, < 7.0.0) middleman-core (>= 3.3.3) - middleman-core (3.3.12) - activesupport (~> 4.1.0) + middleman-cli (4.2.1) + thor (>= 0.17.0, < 2.0) + middleman-core (4.2.1) + activesupport (>= 4.2, < 5.1) + addressable (~> 2.3) + backports (~> 3.6) bundler (~> 1.1) + contracts (~> 0.13.0) + dotenv erubis - hooks (~> 0.3) + execjs (~> 2.0) + fast_blank + fastimage (~> 2.0) + hamster (~> 3.0) + hashie (~> 3.4) i18n (~> 0.7.0) - listen (>= 2.7.9, < 3.0) - padrino-helpers (~> 0.12.3) - rack (>= 1.4.5, < 2.0) - rack-test (~> 0.6.2) - thor (>= 0.15.2, < 2.0) - tilt (~> 1.4.1, < 2.0) - middleman-gh-pages (0.0.3) - rake (> 0.9.3) - middleman-sprockets (3.4.2) - middleman-core (>= 3.3) - sprockets (~> 2.12.1) - sprockets-helpers (~> 1.1.0) - sprockets-sass (~> 1.3.0) - middleman-syntax (2.0.0) - middleman-core (~> 3.2) - rouge (~> 1.0) - minitest (5.7.0) - multi_json (1.11.1) - padrino-helpers (0.12.5) + listen (~> 3.0.0) + memoist (~> 0.14) + padrino-helpers (~> 0.13.0) + parallel + rack (>= 1.4.5, < 3) + sass (>= 3.4) + servolux + tilt (~> 2.0) + uglifier (~> 3.0) + middleman-sprockets (4.1.0) + middleman-core (~> 4.0) + sprockets (>= 3.0) + middleman-syntax (3.0.0) + middleman-core (>= 3.2) + rouge (~> 2.0) + minitest (5.10.1) + padrino-helpers (0.13.3.3) i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.12.5) - tilt (~> 1.4.1) - padrino-support (0.12.5) + padrino-support (= 0.13.3.3) + tilt (>= 1.4.1, < 3) + padrino-support (0.13.3.3) activesupport (>= 3.1) - rack (1.6.4) - rack-test (0.6.3) - rack (>= 1.0) - rake (10.4.2) - rb-fsevent (0.9.5) - rb-inotify (0.9.5) + parallel (1.10.0) + public_suffix (2.0.5) + rack (2.0.1) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) ffi (>= 0.5.0) - redcarpet (3.3.2) - ref (1.0.5) - rouge (1.9.0) - sass (3.4.14) - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-helpers (1.1.0) - sprockets (~> 2.0) - sprockets-sass (1.3.1) - sprockets (~> 2.0) - tilt (~> 1.1) - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) - ref - thor (0.19.1) + redcarpet (3.4.0) + rouge (2.0.7) + sass (3.4.23) + servolux (0.12.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + thor (0.19.4) thread_safe (0.3.5) - tilt (1.4.1) - timers (4.0.1) - hitimes + tilt (2.0.6) tzinfo (1.2.2) thread_safe (~> 0.1) - uber (0.0.13) - uglifier (2.7.1) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) PLATFORMS ruby DEPENDENCIES - middleman (~> 3.3.10) - middleman-autoprefixer (~> 2.4.4) - middleman-gh-pages (~> 0.0.3) - middleman-syntax (~> 2.0.0) - rake (~> 10.4.2) - redcarpet (~> 3.3.2) - rouge (~> 1.9.0) - therubyracer (~> 0.12.1) + middleman (~> 4.2.1) + middleman-autoprefixer (~> 2.7.0) + middleman-sprockets (~> 4.1.0) + middleman-syntax (~> 3.0.0) + redcarpet (~> 3.4.0) + rouge (~> 2.0.5) + +BUNDLED WITH + 1.14.3 diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md index 2f042e7f2..38acc9a6d 100644 --- a/samples/rest-notes-slate/slate/README.md +++ b/samples/rest-notes-slate/slate/README.md @@ -1,34 +1,33 @@ -Slate -======== +

            + Slate: API Documentation Generator +
            + Build Status +

            -[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate) +

            Slate helps you create beautiful, intelligent, responsive API documentation.

            -Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API. +

            Screenshot of Example Documentation created with Slate

            -Screenshot of Example Documentation created with Slate - -*The example above was created with Slate. Check it out at [tripit.github.io/slate](http://tripit.github.io/slate).* +

            The example above was created with Slate. Check it out at lord.github.io/slate.

            Features ------------ -* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print. +* **Clean, intuitive design** — With Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even in print. -* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. +* **Everything on a single page** — Gone are the days when your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. -* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks! +* **Slate is just Markdown** — When you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks. -* **Write code samples in multiple languages** — if your API has bindings in multiple programming languages, you easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown! +* **Write code samples in multiple languages** — If your API has bindings in multiple programming languages, you can easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown. -* **Out-of-the-box syntax highlighting** for [almost 60 languages](http://rouge.jayferd.us/demo), no configuration required. +* **Out-of-the-box syntax highlighting** for [over 100 languages](https://github.com/jneen/rouge/wiki/List-of-supported-languages-and-lexers), no configuration required. * **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. -* **Let your users update your documentation for you** — by default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it's simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to, you're welcome to not use Github and host your docs elsewhere! - -Getting starting with Slate is super easy! Simply fork this repository, and then follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://tripit.github.io/slate). +* **Let your users update your documentation for you** — By default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to use GitHub, you're also welcome to host your docs elsewhere. - +Getting started with Slate is super easy! Simply fork this repository and follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://lord.github.io/slate). Getting Started with Slate ------------------------------ @@ -38,84 +37,68 @@ Getting Started with Slate You're going to need: - **Linux or OS X** — Windows may work, but is unsupported. - - **Ruby, version 1.9.3 or newer** + - **Ruby, version 2.2.5 or newer** - **Bundler** — If Ruby is already installed, but the `bundle` command doesn't work, just run `gem install bundler` in a terminal. ### Getting Set Up - 1. Fork this repository on Github. - 2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` - 3. `cd slate` - 4. Install all dependencies: `bundle install` - 5. Start the test server: `bundle exec middleman server` - -Or use the included Dockerfile! (must install Docker first) +1. Fork this repository on Github. +2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` +3. `cd slate` +4. Initialize and start Slate. You can either do this locally, or with Vagrant: ```shell -docker build -t slate . -docker run -d -p 4567:4567 --name slate -v $(pwd)/source:/app/source slate +# either run this to run locally +bundle install +bundle exec middleman server + +# OR run this to run with vagrant +vagrant up ``` -You can now see the docs at . Whoa! That was fast! +You can now see the docs at http://localhost:4567. Whoa! That was fast! -*Note: if you're using the Docker setup on OSX, the docs will be -availalable at the output of `boot2docker ip` instead of `localhost:4567`.* +Now that Slate is all set up on your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/lord/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/lord/slate/wiki/Deploying-Slate). -Now that Slate is all set up your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/tripit/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/tripit/slate/wiki/Deploying-Slate). +If you'd prefer to use Docker, instructions are available [in the wiki](https://github.com/lord/slate/wiki/Docker). -Examples of Slate in the Wild +Companies Using Slate --------------------------------- -* [Travis-CI's API docs](http://docs.travis-ci.com/api/) -* [Mozilla's localForage docs](http://mozilla.github.io/localForage/) -* [Mozilla Recroom](http://mozilla.github.io/recroom/) -* [ChaiOne Gameplan API docs](http://chaione.github.io/gameplanb2b/#introduction) -* [Drcaban's Build a Quine tutorial](http://drcabana.github.io/build-a-quine/#introduction) -* [PricePlow API docs](https://www.priceplow.com/api/documentation) -* [Emerging Threats API docs](http://apidocs.emergingthreats.net/) -* [Appium docs](http://appium.io/slate/en/master) -* [Golazon Developer](http://developer.golazon.com) -* [Dwolla API docs](https://docs.dwolla.com/) -* [RozpisyZapasu API docs](http://www.rozpisyzapasu.cz/dev/api/) -* [Codestar Framework Docs](http://codestarframework.com/documentation/) -* [Buddycloud API](http://buddycloud.com/api) -* [Crafty Clicks API](https://craftyclicks.co.uk/api/) -* [Paracel API Reference](http://paracel.io/docs/api_reference.html) -* [Switch Payments Documentation](http://switchpayments.com/docs/) & [API](http://switchpayments.com/developers/) -* [Coinbase API Reference](https://developers.coinbase.com/api) -* [Whispir.io API](https://whispir.github.io/api) -* [NASA API](https://data.nasa.gov/developer/external/planetary/) -* [CardPay API](https://developers.cardpay.com/) -* [IBM Cloudant](https://docs-testb.cloudant.com/content-review/_design/couchapp/index.html) -* [Bitrix basis components](http://bbc.bitrix.expert/) -* [viagogo API Documentation](http://developer.viagogo.net/) -* [Fidor Bank API Documentation](http://docs.fidor.de/) -* [Market Prophit API Documentation](http://developer.marketprophit.com/) -* [OAuth.io API Documentation](http://docs.oauth.io/) -* [Aircall for Developers](http://developer.aircall.io/) -* [SupportKit API Docs](http://docs.supportkit.io/) -* [SocialRadar's LocationKit Docs](https://docs.locationkit.io/) -* [SafetyCulture API Documentation](https://developer.safetyculture.io/) -* [hosting.de API Documentation](https://www.hosting.de/docs/api/) - -(Feel free to add your site to this list in a pull request!) +* [NASA](https://api.nasa.gov) +* [IBM](https://docs.cloudant.com/api.html) +* [Sony](http://developers.cimediacloud.com) +* [Mozilla](http://localforage.github.io/localForage/) +* [Best Buy](https://bestbuyapis.github.io/api-documentation/) +* [Travis-CI](https://docs.travis-ci.com/api/) +* [Greenhouse](https://developers.greenhouse.io/harvest.html) +* [Woocommerce](http://woocommerce.github.io/woocommerce-rest-api-docs/) +* [Appium](http://appium.io/slate/en/master) +* [Dwolla](https://docs.dwolla.com/) +* [Clearbit](https://clearbit.com/docs) +* [Coinbase](https://developers.coinbase.com/api) +* [Parrot Drones](http://developer.parrot.com/docs/bebop/) +* [Fidor Bank](http://docs.fidor.de/) +* [Scale](https://docs.scaleapi.com/) + +You can view more in [the list on the wiki](https://github.com/lord/slate/wiki/Slate-in-the-Wild). Need Help? Found a bug? -------------------- -Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. - +[Submit an issue](https://github.com/lord/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. Contributors -------------------- -Slate was built by [Robert Lord](https://lord.io) while at [TripIt](http://tripit.com). +Slate was built by [Robert Lord](https://lord.io) while interning at [TripIt](https://www.tripit.com/). Thanks to the following people who have submitted major pull requests: - [@chrissrogers](https://github.com/chrissrogers) - [@bootstraponline](https://github.com/bootstraponline) - [@realityking](https://github.com/realityking) +- [@cvkef](https://github.com/cvkef) Also, thanks to [Sauce Labs](http://saucelabs.com) for helping sponsor the project. @@ -124,5 +107,5 @@ Special Thanks - [Middleman](https://github.com/middleman/middleman) - [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) - [middleman-syntax](https://github.com/middleman/middleman-syntax) -- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages) +- [middleman-gh-pages](https://github.com/edgecase/middleman-gh-pages) - [Font Awesome](http://fortawesome.github.io/Font-Awesome/) diff --git a/samples/rest-notes-slate/slate/Rakefile b/samples/rest-notes-slate/slate/Rakefile deleted file mode 100644 index 6a952e1e9..000000000 --- a/samples/rest-notes-slate/slate/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -require 'middleman-gh-pages' -require 'rake/clean' - -CLOBBER.include('build') - -task :default => [:build] diff --git a/samples/rest-notes-slate/slate/Vagrantfile b/samples/rest-notes-slate/slate/Vagrantfile new file mode 100644 index 000000000..43b1f9954 --- /dev/null +++ b/samples/rest-notes-slate/slate/Vagrantfile @@ -0,0 +1,39 @@ +Vagrant.configure(2) do |config| + config.vm.box = "ubuntu/trusty64" + config.vm.network :forwarded_port, guest: 4567, host: 4567 + + config.vm.provision "bootstrap", + type: "shell", + inline: <<-SHELL + sudo apt-get update + sudo apt-get install -yq ruby2.0 ruby2.0-dev pkg-config build-essential nodejs git libxml2-dev libxslt-dev + sudo apt-get autoremove -yq + gem2.0 install --no-ri --no-rdoc bundler + SHELL + + # add the local user git config to the vm + config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" + + config.vm.provision "install", + type: "shell", + privileged: false, + inline: <<-SHELL + echo "==============================================" + echo "Installing app dependencies" + cd /vagrant + bundle config build.nokogiri --use-system-libraries + bundle install + SHELL + + config.vm.provision "run", + type: "shell", + privileged: false, + run: "always", + inline: <<-SHELL + echo "==============================================" + echo "Starting up middleman at http://localhost:4567" + echo "If it does not come up, check the ~/middleman.log file for any error messages" + cd /vagrant + bundle exec middleman server --force-polling --latency=1 &> ~/middleman.log & + SHELL +end diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb index fdcb21f32..0cb5fea49 100644 --- a/samples/rest-notes-slate/slate/config.rb +++ b/samples/rest-notes-slate/slate/config.rb @@ -17,6 +17,11 @@ # Activate the syntax highlighter activate :syntax +ready do + require './lib/multilang.rb' +end + +activate :sprockets activate :autoprefixer do |config| config.browsers = ['last 2 version', 'Firefox ESR'] @@ -33,9 +38,15 @@ set :build_dir, '../build/docs' configure :build do + # If you're having trouble with Middleman hanging, commenting + # out the following two lines has been known to help activate :minify_css activate :minify_javascript # activate :relative_assets # activate :asset_hash # activate :gzip end + +# Deploy Configuration +# If you want Middleman to listen on a different port, you can set that below +set :port, 4567 diff --git a/samples/rest-notes-slate/slate/deploy.sh b/samples/rest-notes-slate/slate/deploy.sh new file mode 100755 index 000000000..909a9d908 --- /dev/null +++ b/samples/rest-notes-slate/slate/deploy.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -o errexit #abort if any command fails +me=$(basename "$0") + +help_message="\ +Usage: $me [-c FILE] [] +Deploy generated files to a git branch. + +Options: + + -h, --help Show this help information. + -v, --verbose Increase verbosity. Useful for debugging. + -e, --allow-empty Allow deployment of an empty directory. + -m, --message MESSAGE Specify the message used when committing on the + deploy branch. + -n, --no-hash Don't append the source commit's hash to the deploy + commit's message. +" + +bundle exec middleman build --clean + +parse_args() { + # Set args from a local environment file. + if [ -e ".env" ]; then + source .env + fi + + # Parse arg flags + # If something is exposed as an environment variable, set/overwrite it + # here. Otherwise, set/overwrite the internal variable instead. + while : ; do + if [[ $1 = "-h" || $1 = "--help" ]]; then + echo "$help_message" + return 0 + elif [[ $1 = "-v" || $1 = "--verbose" ]]; then + verbose=true + shift + elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then + allow_empty=true + shift + elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then + commit_message=$2 + shift 2 + elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then + GIT_DEPLOY_APPEND_HASH=false + shift + else + break + fi + done + + # Set internal option vars from the environment and arg flags. All internal + # vars should be declared here, with sane defaults if applicable. + + # Source directory & target branch. + deploy_directory=build + deploy_branch=gh-pages + + #if no user identity is already set in the current git environment, use this: + default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} + default_email=${GIT_DEPLOY_EMAIL:-} + + #repository to deploy to. must be readable and writable. + repo=origin + + #append commit hash to the end of message by default + append_hash=${GIT_DEPLOY_APPEND_HASH:-true} +} + +main() { + parse_args "$@" + + enable_expanded_output + + if ! git diff --exit-code --quiet --cached; then + echo Aborting due to uncommitted changes in the index >&2 + return 1 + fi + + commit_title=`git log -n 1 --format="%s" HEAD` + commit_hash=` git log -n 1 --format="%H" HEAD` + + #default commit message uses last title if a custom one is not supplied + if [[ -z $commit_message ]]; then + commit_message="publish: $commit_title" + fi + + #append hash to commit message unless no hash flag was found + if [ $append_hash = true ]; then + commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" + fi + + previous_branch=`git rev-parse --abbrev-ref HEAD` + + if [ ! -d "$deploy_directory" ]; then + echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 + return 1 + fi + + # must use short form of flag in ls for compatibility with OS X and BSD + if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then + echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 + return 1 + fi + + if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then + # deploy_branch exists in $repo; make sure we have the latest version + + disable_expanded_output + git fetch --force $repo $deploy_branch:$deploy_branch + enable_expanded_output + fi + + # check if deploy_branch exists locally + if git show-ref --verify --quiet "refs/heads/$deploy_branch" + then incremental_deploy + else initial_deploy + fi + + restore_head +} + +initial_deploy() { + git --work-tree "$deploy_directory" checkout --orphan $deploy_branch + git --work-tree "$deploy_directory" add --all + commit+push +} + +incremental_deploy() { + #make deploy_branch the current branch + git symbolic-ref HEAD refs/heads/$deploy_branch + #put the previously committed contents of deploy_branch into the index + git --work-tree "$deploy_directory" reset --mixed --quiet + git --work-tree "$deploy_directory" add --all + + set +o errexit + diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? + set -o errexit + case $diff in + 0) echo No changes to files in $deploy_directory. Skipping commit.;; + 1) commit+push;; + *) + echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to master, use: git symbolic-ref HEAD refs/heads/master && git reset --mixed >&2 + return $diff + ;; + esac +} + +commit+push() { + set_user_id + git --work-tree "$deploy_directory" commit -m "$commit_message" + + disable_expanded_output + #--quiet is important here to avoid outputting the repo URL, which may contain a secret token + git push --quiet $repo $deploy_branch + enable_expanded_output +} + +#echo expanded commands as they are executed (for debugging) +enable_expanded_output() { + if [ $verbose ]; then + set -o xtrace + set +o verbose + fi +} + +#this is used to avoid outputting the repo URL, which may contain a secret token +disable_expanded_output() { + if [ $verbose ]; then + set +o xtrace + set -o verbose + fi +} + +set_user_id() { + if [[ -z `git config user.name` ]]; then + git config user.name "$default_username" + fi + if [[ -z `git config user.email` ]]; then + git config user.email "$default_email" + fi +} + +restore_head() { + if [[ $previous_branch = "HEAD" ]]; then + #we weren't on any branch before, so just set HEAD back to the commit it was on + git update-ref --no-deref HEAD $commit_hash $deploy_branch + else + git symbolic-ref HEAD refs/heads/$previous_branch + fi + + git reset --mixed +} + +filter() { + sed -e "s|$repo|\$repo|g" +} + +sanitize() { + "$@" 2> >(filter 1>&2) | filter +} + +[[ $1 = --source-only ]] || main "$@" diff --git a/samples/rest-notes-slate/slate/lib/multilang.rb b/samples/rest-notes-slate/slate/lib/multilang.rb new file mode 100644 index 000000000..624c6e495 --- /dev/null +++ b/samples/rest-notes-slate/slate/lib/multilang.rb @@ -0,0 +1,12 @@ +module Multilang + def block_code(code, full_lang_name) + parts = full_lang_name.split('--') + rouge_lang_name = parts[0] || "" + super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match| + match + " tab-" + full_lang_name + end + end +end + +require 'middleman-core/renderers/redcarpet' +Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang diff --git a/samples/rest-notes-slate/slate/source/api-guide.md.erb b/samples/rest-notes-slate/slate/source/api-guide.html.md.erb similarity index 100% rename from samples/rest-notes-slate/slate/source/api-guide.md.erb rename to samples/rest-notes-slate/slate/source/api-guide.html.md.erb diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png index fa1f13da8193dacca747df56cc1f7bb0bc111a7e..68a478fae3611d878af18c575cf8218b818f41db 100644 GIT binary patch literal 22317 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
            hg5Z+!{Sa)O9QNxi$tfwtDZos~b$g zf~%$5p41?m+pKz)p$R;(OqAz%NpBzC-Bs|s=5}Fzx{?20LS6w?=8B26nsri7pgQJ;3C5bj0$M1LtS^c{;>_#l@s`gohuYN|zamk(kUh|tL7}g=G2^;)T zUeU-G^*(GZXbEnp8=VhipQGRpEcT%zP$hEGm7S_jENi};dXF4YE~JMgi|Exc7>E5T zb4#?cV^K*FT4Iwbzn+t~$3Qv%rDtC+iB%YXhLdUC{%$m}ruUffNRcHVvPJ%0=IQ&k z9?VV+SK>;krY?^g89zh1#h|K&9TQ zYDRz|t5`ubY6z4BGJK1jhHI0bmu*XuwbnjvGRn%l8qbMM1tsl`tetlhb0vsl`B4); zE#{rhT`06C@8aH^Voznp;rm-!_ikp7iME;3qaYW;HsrzZDomfEQ03=W5i6|cQz+s9 zUo~5bxg-uq1fnQZB4>(Gi~Z0P9(Ixsrn8Ei(}Co24=Cl{pIr)Td?YnY;bD5rhS-JEa3%c%9-tHAd4-hKh&vo)8l%bBQ7v$7%XmKi0jYZXS@ zWG5JWdM#@s)Ic!Ae#4vW`iIFT^raF1Nx$Rf`Ict6!AUcf~GCWX8>1OEAjAA z`4e8So@KJO(KVab`gGRsv3@&dGMN#R!xOD7NV`|2Lv0)37@e;5$M2#363V?ojwG|O z47H#0L!+;=-jO1{eeJZ}<&2BPL6rkDFF=G?UGlX-aAJ+vx-E2it{dVUgGL~rG zoqj=Yk8m!7&==0dFS4R4l%8{68LdZKjEVSq?lWyql9-6y4?e;A>uC}6yBJfb^$n&z zKFSJjT=Z3!w`$NJv!t`4g&DMXL{rrQo3G0EHzvRK_{6(Z%KCTbgc`@{r>Ceo?IMe+ ztO?z8wx=wt6}f~-IOxcqmheaPack{kaFx@J_?^8-VyxM5?=Lc0;fG%y_&r9=GfbsD z1a(>R4F%`D-Yz?SJ)Avmy6@QYc3)}JCn}v0yKFD0Rbe@_c=#RrDFa6SufB$fRObW8 z6xx2g+F?b~*HSM~=kZ|JpkohZr{d=~z!aU4HTDAccuBK(XLXE(aV~03UQe*G+pi|m zRJp>Dvi3lqrAsowwVQE2vp~HgFvkbPC-eD|1bHUrkLSsjD9AnCgg3mXpA3IE*2Xy6 z5TeV{|8cDg$){51%Cm3(TCT2ZW4Q)ouve(#TJS{2Sd3!VQT8mj|{J@P@B%=9sYjut=gZE|TDiBJ<7@;(bJ_jQL$R zz{mWyO6!xn`^s)!f~bz(^O>pv`_pgeh&ZS=}l+4QRzk`0knS(nO zv_G#7s8Ua3%E%js)N)bcxm*#J5P+bQyrM{{X{|njlk*=GXnxo@6kllqaKQ(JQMsN+ z)LEH)Lp#4MX&B}7*_~I~pv>42+p> zU08qLH2D*qSwnLpP-*#v*f}S*B0}RU;2Ny9Ge%{S78l{D#ay?}VhHv3NUi#LrnCml zejqp_Ns9lH$~S=dic{?GD$MxNYUGL=27|uZr_2wQZ(Na}#3F^m(MhYz+8B{*cpya9 znt9_C+hHUY>t@GxY+0i^{4F^2q?CNxioEXqcc43_Er~|;ui6F|_#p?9Pa|rX8U)tl zB-}$94~EY2_pliK7EB@(;|)<5v%+MnGJyVfjSR+Tx(KL&g%F;(sbALabvd%Fx-L2> zhvc@d$uR|+8)$m9PY1Z+*D}<%?-B)#R;f$Vl?+u!{QLOwG$K{!v}btd$0l^wIZi}`SLI`KAoH@odD&^$rO}l;`p)v~%>u)=^@wgP9jpTj1rJ&4C*zVhaEY)m{6hrPlLzedM7u3~+H}=A z$DGFW6?~3xP6ckG+_6^x@#Wtafi z?WoIE9#(F>oJ-^te$A^BL@BTccaxu(Gky_}*1NlSc&IWsY8@_Bwz8yGYJFJ$W&JMG zO37l#o&NRoM@ZJxF9kK+X{T|sci@T<_7hac55JM1(?gh>Q=grz;y%0vN9!GP#tr|b z%WFvf>Wr!gSymB^+mY})>6P|(?4!|n(Kl8U7@sNr$jubKfhqQ?ciD1q6`+ZH2XQJg z!~}c+u&@o>a}bO8y~?%h)>L}`G(Ksde&5&Q-LBe@EwigmSiFAg-LO=OUu1U|rYvRX zNm!KU#rK~%NpLC%;3d*5I8yGtVKCDg(KV8@?JwX|7(czwXZgZ13EGtr>sWA&=gi}#=l0D$Pt-P*!Rv^?g8Wnm z@e$FRlV=pW(6Psr%)G+f6n0)`$Pd%nTKftW0NLo|ZT;aF)$2toqs2Czshtn<}<;k!zWe-PP1CJGTA%>V{Qe+Ov3%>Eh$4HPI-L zcjHB#);{O#)+b2;vindYsHG84H6XPW*#jJ0&tuJ`V4}21P zFORC8Pr!==fxDCfX8`x@Xsb#d$FB6$x`<~~+DuXy^s`4vaWq`+=Y&b*{5VRPL(y^` zAtQi8YjLfHouW@W=`4Hs#N3q!h&B62Wh?4u-fLa;#asB*$TopG0~6@$xQd$Gnc@(K z@cNZct#@`oe}k`6Hi$VJ2`gMEwAsOi6uk3kNqSDAp+s8ZuD@yR=I{AQ%W7QRI!E?c zkN!zj5*-6WgMPe&l#silVR{CeN(MBOw*K*}cUa6dx!jwX{Vg_ao6M@`8f!49)!rJE z&3CZ>s$2iv?D$Zy*t(QQ4I>ig=g^XgT`I>2$D-zs8`11#MM)t$nK_Fmy3BP-_gFto zN#;t8E$c1TsYx59$YIL~O^J$cn4ZlUW{GSOB29#LHuEXDnAP?yT#6(3>n5iaFciOx zWz-+7wmEHgVCe4r>O|d$m=^c41Zlfn;k_*bS z@|PCG?Y|`~4hah_00XJt#m~!8Q+eZD zAy-VNpOZ?3`&#>32?jT^uz`tAWTY)%drA|N&;Na}#z2RS2lrdcM+TtcME;8a@A_v| zA|cr{Mwhe^ks{RnrY9RRAdvx;SBLSqQk3s6E8M?xRlB;w)xMV5k9smX2>tqIt zo4I@Vor{Oor#Iifr?yYe+?kZ|3|{xQN>u~lsaxi?oS$FKx`vfc1!c_ZZK+-7BcQhr;-a}8(@@73qbT3kqqb-yE zjQ{Z72Ni^&H7^GP6WxDwXQ$x3kT&MXPaj*%2^;mEac#&vJtH)or}zJ^lN;!WVZWX! za9Bwb>?PSadtMp`HKANe+H0RPw+8|x1-luLYMwve_AINXd(jx<5==(T{tM&Zo zeoObG=bgU?G&PsVdYP#ornW4drPls%e9P@ya3f*Nc6nbhBj&1!#(w#X*2+#UQ=-ynh31gdV%&hvbKV3NY)9@-$v`;?_|E zdVP$fT{8KXZd`tknyZwn9Ilkxe^*Y?)sDADkt3hLBWGj~v6iP%4`=`dM_r?%E)WSFj^?b37B0aT&T#TW2vctPUCo2;7yw{|f-~D|fVu z=(^F}(SAxAX0oq1(X<+dE6xl_^D~DoiLG&!VWxjb!AmM*Q%<&m*6dAFh7WtK#S2o! zB3Qrgt?Ay1Y+xu^ITBExRD9}(D$hIIS3<29mA7LO=3G|Da)w`jYEC$hJ>a7{Y9)Ge z{!rZo!l}dtrwNSev>*p(;rX z{=3*&okSZ{ef{acKi7TS0?W+Tr1h1r*_avKClr~(`@09tHOW$>^v)MSeI@t^yp<+j zQ~mmvajMUp?_O5=gQ}m^dH8-gT0fa58jq(lcb7jQf$#Bg>7{3O-{Nt%-m2Cm-_;j3 zebwmN=5$RpxJVaR``Z%?m%_|)?0!3hAt_YnG4NSqF}7E&!XAe8#$71bV;I(Nhi)qJ zjL`cHR=3ZTmNo+-nQMoncao`(WoMss+S;p4>qvBqEAu0v&Q8%M&OT`jod;{LqwCdd^}0Gm<|X;n@JhI>a@OSs33;0#KhMLN zkvYeAz*1RHJO6jK_@C6m8A49iZP<;$-O?2-e8T0=gGo=?{vD=V($nX5xuiA8`2Mka zs#zIgjx!=SxL6%dGYeg1!Ve9F_#N#mzCZHXr}UFZo48OwiJj{4*Of}AOqrM~HJ~bQ z(vIM3L*=I=GzE1K@rbr<6vu+;6>4sn35&jI*xcU0my{G1IpG#Xy`L^33fJd;ng}%b zk+*%!ay`?GB}f#PSJ3^H&l4q3Ib&T!mD1Jw_Qpy$p&Qae#!bDteCiCV%eO==9fzlO z9c7;OPfJ{h10DGq#RI4<&cSDXCyhkb&G`-saMq6_YhP5KJ(gkSjP3r2B$qMCJ2#Qk z(YuXJ19q}&=-jOQ{U>z!BXsS2bO#4ctdGk5_dheV72<@n!8Ee!H>^F6%1&oF%c;x^ z{C;t|&To@SX`+j2-kX0Jw`=5+zHuH{xIHao1_vY}aRUi}TB|BCL}csMA4!UYk65Mu z&cFU?vQSRSTg1T3HY_Z5{49f4CN68%#uedK7`x1TNJDpX{r&IBK9Df{C)8$&>fNO8 zIc=lIN;2n4z}*VnR-uP2Q2bs9PG8A$~ zpwNOo}sg^=@7h`m*!TZRzUoHI<3?aWz5+ka9%T6({?YiUA$cAIF>G9zhgFwl5R&MT<@7)e=i z-kzP*pttn5-J$2`Ds;Mft0P=_+t1}P_0V_i+;A_w;H{e|W`|^VmnD3eX{Kz;rVf1@ z@VEOTlzeac_#Vv)guV ze7UL5vm`1xjj2twRcax@?@3u_fcH>gb}CYp($L*}J*bLf`}t3LfBT^=I)H5$R_FWG z0IzOmLP(*G2K7gu5zViF2RZqk6CEeN^d!PA+^744&-X{2qKN$3B=T@7wQKnvass#g zzTGNBnHTOI124X`C@k7&PPkihGX5yj`7*eD=hm?90L5FO^lQamw$HuP~$f#RD!1d zYWECz{{3AKs&v`Zplg`Ydou7Z4unfW=#}i=xjrKMO5Vzhv`(mYdJkMTDA?UsGi+U) zedONW7exYLj(8xMECKzQSvFd(@^uo!g>{fb@7VK7r7$G(n1YQ0gn(NAiX4nK@Uve-OMyYi58 zW<^nFJ8kFf#0`b3kon#4iWx+srw+b9%);GBle^$rXun|9>w4m;1}NFIvYNJ#-T|5% z`0%;meMrVZ-_BZayOV@3O>3}Tt~(={(o3n!n{5HzH0m`coh`BVp6#!5UUbN_>BB@u?iD#Qe^=3xVo2*B? z#Gjfm&VQEBK548hXpdHYXo-4qB;L4qX_WW%g(ef_!6|14t75LK6GPVZZ-EG_zO|5r z`D31JWSS=yfBGmp%TX{*eCJ`rw=?PuuX0J~9fZ&Gf-974&eSXeiQ1%6TIhy=CT5=f0kZ+GreHr*Q`X#_I^ zAzBNX(vIrOsdz?Ai#(IEoK_8>MwY3#Vo-r&xs3&}aqg=U67qY8D!Z(jVsXQoXAdjh zQe>!^Qw7-JvoUH zud0$NIqIBO{GR<*ZzyPo-KvI%EAVTwu@av|d|s8_RWU6%WZC)D%<3$tD?N!x>1ML2 z6YKVi2OXqDl&AM_!j-oX?*yl?EL7?*fDDf%)gDQ5V_#`66K)DS7SXc$N+-G=rMgMw zeTFebq7p)7=*c35{n^NfR&< z)rl>OH3xHyUV1n7x{!8fS@zxXeBkwjPWuDQ4~#x6QAW?)@}9_FO9O*2PC5(NSK(z& z>mbQ-Ve?D4=@@_88X2_?67i?tI^*1yWZP%dH)!HYOLri%zj&@UtLJ-K(Qf0hl;I`}mk`cS_iTM7rv!79T$VK<^#>EDT3vBM4wt+m?%xNFUYL+BeY;z^jtfEdiglxO`qP;3D%0fGrjXE zg!D+t;{q@3Y_PUUtEqTwZTvoi&pR!~KmZt?Sz@&QS$E)6x|#U7FgF7$oa`h5s31M- z*um<^Y+SK%ban&x!!nhvz#PyK|8NHnd5ZqFQZH+;sD% z_hf)BFAOM3sSiOWtJaxIUOb5*_mazzNkcG8CLe)Gs)3EmRB@U!%bZDnxFt`P zLYYr3yn)tewRuw8r&=i|ns>zUF?3j_CdpQvboGX4s%|iRN`G8(p*B7Yd2PQjpBtdb z3(YfHFTI-NZ*__?Rg(oS@|kD4y&c2Pao#Cn2jO@dN>P~OSgSTFo?6*(Un#A*bZp9Y zxn$-u>0>2~I1guGKySX|pW(rz&!Q5pzl(Hau$&{5tMu~m!(UsG zaoeA=_h0Q-R}26rxt=M9p6|}Ndp?O9#=qYkw?IbGh zy1u!Xx=3wLx6FV#WHPRDxzfAh&$G|pWIF>{T!@P_)GAE+PPGfYywR@gT7Dj>huP>= zvYLS_25*0F!QpUAJay-&GgPFC@a7tWt1ZXXQIZ$8lBP*Ia51^`Z&cn%&3w71wwuln z6LmiMG7x|e?$oQ@#AfmAL?5+4(P)37JLE|hSGko8MSZ(G!Ijp9E=v{xY@1AmO^%&VnGZT__ z@V3ms@erHoqBQqd4_T%CE~pSJG`B~|{pV@Eh0&kpE75LTtLes-Z4SvYm8!2*hv8cq z=T9WK$#r3|Et$)spM~_j;2iC>h>`Ei`fzt1BU_8EKka(-$Y|I21S^djms3`PEI=?j zy>0;l;XeuYTuII+&hoM>eEFSYs)o)z4h&%&d+c{{4bs+5bjoGujmvWFV&lTfmdYwC z?t{RaYF*RC9|0fXuEh{x!Tt5T_9LYzDJ!O0ayT=2l=-lNke$sxs&`69RA6@z%! z>0OP=Rts2np+?WM6VthIn=wGn^M;Ej@TU(&{87&UprMx2h33srC_NKsqA!Pi9OCpu zkJ^%K7!Mfe?GJKkL!MkabaI3oWICL~Ovn|aH);w-rNS{Wp@R*>=~S(^xa2j&Sjuhx z_ZaytkNL2SxeWPoud%w_v(5-5#!Ia};MM!tswhWgk;X=W8&&d{=o(<)TXN1>hd+}@ z)lvhSYko9@M-m8fo0ObgJ0~x_cwkRTJP&+nKk+Lp<*-jZ2Vapz+ndl-IsDO{s;e`U z4t$%EfiN&`*y+VT>TDAzyq7Ijl7TFDCtY_s=7Qy~E>Wu2q~e)V(j-sxirjvUXyHoY z6$2}-*BcFM+ck3D$yYPod*frE-xKznDUFhj(uw2w;`cl0?B}Z?m4my@(z6^~STC(g ztdP{^Ys-QM=Rf+Us~_vO2i86w&f1bt~8EnTyzL_g--|{ z9nVx>=l1(~HqTB&DV;xb@Rv*nvmDJU%`33Mj+cNL_Tcg)k5&^+!^BmMar>?TUnwKz&qv9N3|Jj3c&L| z>*(keg~1g!gK+-Gj3d`0a|drt2Ph+1eg;3|FPtfATSWcrDc0_|=)FvILvY0ATgUcMa7@ATTWxOU493lGQc2-Atku>vjmO91y z!?u`)ck`^07!7MSb6ugaZ?8IoxoB8>@+E3+ni>#qudw&>~NYjS_f7N8b=8WsF zDjY}y+agAyI471;MdJ0$)Fi`v<`o&yfGiJ;mt*inKa0Q+E6VYKA0Hvc`;??*^L+g$ zzoV1Ur~^vFB*Ja1FnaN-y60k zH?lnLkM2i>xf1$Yzn7gSD7zdqHst2a*_tD^@~m@({8YWCkxT}?V%O;@V1L<3Y86L_ zo9<876ChnrXK9=+WYqo0gLPk=Rg zbi7J?5aFy~W~TUDG825HvN007Ap&>u7Ir+n`Kf>ViN1p;!_?N}9`H(FFo5GLjfsqR zkb>hKMtN(T{{_LtAQ>5_-r5B%>Fp#FG4t z61LJ3@5f4L7K7G(amMN3=!sU36fAo2^_1YBtQhrhX6H)S1+X*5sEHxPRQ48!rp{Vq z*=stS65K@YZ0@IZ#HI|c)`?yr2zZkY(Q;iu#JfQnm19hOBCp&#(bKYX{?Z?a@ zzEfQ>oR3MIJUaEGIeUi(-Qi}`{}(--BFvDHAUGv?wo0y|L&8PbBlJ)ARAQUF+MpsPS3x+00!x4ls~b4 F`9JqINYVfR literal 3507 zcmV;k4NUThP)L#ooJEqksZ7R751M$tG^1>19)l zDQij3NssFqW6JM1bKd*j`xN*N5J{Lh|HGL#@6NnCcm8wlo%s;Ir)g;Xm5|rxuc0Zc zp`j_Op`j_Op((4Op((4Op((4ODXXEODXXEODXXC=n^nBKOQbCm;M|IPyuGwry?tus z1JLdN$$VPjw=)^a=PnI@NHPqoDVwc$WsAt4Psnm?f#ubW6QZKGdNq5{1JLbStT4*$ zlnmu_mxdTtmJ&rZWwRR-dkI;p`iPq+&GvAA)n36{{FtOEt0|jnDEsYEQQA|;5*eTx zKX^j8yQ#*6a9T8FHDzTfz zd3!ukB+lcn(ZX8N0?6tN8T0Xt?P8w4*gVN4Lb#-o!Z%06fiN+1fLJ?DytPAoe@toh zpJhzS)}V)8a+IXXO2Psq}*jYx@7^@47k&n%KB7m4zvgk$k=7i-2E zhY#tn`D=E89J25UM;RWsPZjy{$&KL>AzV`lyuM9T@v#W7_Oi?$YDSxYF2YvYHV~Kh zWE*8;=kRD5zja;lMS^;}b&6_?3loj1srvb~is(ICSvtBE5WA-5?Mtp#x0>fLZ%mdEF1OxfnOM2p%c>Q%N=HhGaTAm<0uN7St(?xb2M%b|i|P!><$s*a$r zK>gc_(T@qQV)7+%BX8f!PUGey@`!lgMrN-#SMWyynONnK*fe0`X_Me;a94RNcEz^_{fXW^2|6_v*Ls>kQo z8!|X}3uQ-jQ;+XhkDJ9k1f@>JtWuWzM;0;}zGwvsTa0hJKWSQ1?eK7a=f3!FP1*ZU zmYk_*VL_=}#&Y#DsJ-kVZ;On8Xx`2|v-+Ntw-$gl31Y}2=vLe$<^p^_Q*xWK=jT~5@?2FecaECRb&0RJ+n5;=M?4<$yDurN?6X`w8n@o<-i zPDS)D6V0>T@yIMvmffXBBRP%<-%W$#N_ zvQqPQvrs{zzwrYEcVtS2pk^!Oyb#DLWt~ewx6G;pjz)+gg)Envp@Z$0T~P(7{v>ZDnawpPW8D$^w#gqZl{496e7i%Jctxm@Z|N#|r9>xlOBeZ7Sa1 zYdOV_?`f_#w=f^>g8?~1*^E^(@!lRm^e4!|(Tf|zg$+u6@d#7q)MCq7hx_S`6LDkN zLRo+*&5NwDV89FKiT_9?ZdiS>$Hw&1iY;?HWEgz-D8sE>Q&vM$RzpKmRzpKmRzp)(L*s8l*-sKZ(>BzRxDZ|Sm(&6ocKPT)sSD9na_5}S zZLan9{&LLaPHO&~`^ww<%3j=2`Q=>|--|7iOD6vP2@g5z+ouc2MSO8*w|rK|47(g(-C*97?zxb!Kc99RH>&^jM6Z~r=5pC` z2uo&meq(R>WwScPEoqud=Kj^uVsh5&=eFCqpcO3(L%JSX-YC02q*njIQEn8jAKgD?UA^DZ@`sG=y?I`npb-OEq#McZhSDkP>P`siA3SovhPiEj zPs{ht{<1U2_Z$^Chz&rmFOC$C4CzL%?GX<<5@^rDmW)itr?=GPF?D@i>Jvuyqr&6H z@U{#~W$&VviOU;Pr-gA&tZ9(p`2XHs4dE;CrBL`*irdWby`m!?4jwshZCLwVk*(;6 zBw}yvD?5E$FVtoh-zF8g8t=uhY*+fSZxKjZ)d1?x7~czT>a6hB$BIrK(-#FKW$&CR zz*mAs44O5eCmJGfep@xVGx*Y(ou1lU(^&vW>#dmGF?3=N=Kk!~>e9}zjTtd7^mkSa z{4d#^rMVni8r_ip>S)pDqpKn>XMJ{Ktx8 zvHxg+2kb&j%)aFDQj8D6dl$CAB2sC&cO%^`c_pWa%lK%Jzpg)9gr0lD^E8>=TB&(w@6-@u~ zu1Zlex-6L1ZDhb;+!B0(!3-F`tO>TpuLKVFV^~%iXURr&R2VV5?!7?j1MY?+oGSZ_m5xCLcx0_bT_Hfl!ayEH0H`_+U}{AJ700+sQdHpLu6^P=fpnF1~uU<9&i zW7dv|0b9$kq|tp<=%bh`Y?3capK1Hs69eatHf@~+awIW0vhtxR!F{mc@L@v;VMH_% z1LoFd`iZ{ppX7RybWAt(2{B^OU5l zw?SD#6;W00@L^1iM<{C(z^dmq)kJ@$#vWs25*7Rq=O@&o&d4M!P}ZuLN!gDRO9l+{ z!<&41$dhl42=HSNW7ZqO+mXc~X7iUuccKkj5o0n(44ygr+bQeZ(Wb4lKu%DWwXceU z!KQdzrbHq_8i(IP0EfXNSQOTu$4!3&-d)C?UEa8{VGe$TrIcxlL!(y`}Bz9W9^-DuTQ&vMmQ&vM$RzpKmRzpKm hRzp)(L*uW2KLF&#KY$nt%ZUI0002ovPDHLkV1g^hxJUp1 diff --git a/samples/rest-notes-slate/slate/source/includes/_errors.md b/samples/rest-notes-slate/slate/source/includes/_errors.md new file mode 100644 index 000000000..267e352f3 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/includes/_errors.md @@ -0,0 +1,20 @@ +# Errors + + + +The Kittn API uses the following error codes: + + +Error Code | Meaning +---------- | ------- +400 | Bad Request -- Your request sucks +401 | Unauthorized -- Your API key is wrong +403 | Forbidden -- The kitten requested is hidden for administrators only +404 | Not Found -- The specified kitten could not be found +405 | Method Not Allowed -- You tried to access a kitten with an invalid method +406 | Not Acceptable -- You requested a format that isn't json +410 | Gone -- The kitten requested has been removed from our servers +418 | I'm a teapot +429 | Too Many Requests -- You're requesting too many kittens! Slow down! +500 | Internal Server Error -- We had a problem with our server. Try again later. +503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. diff --git a/samples/rest-notes-slate/slate/source/index.html.md b/samples/rest-notes-slate/slate/source/index.html.md new file mode 100644 index 000000000..fc26b1964 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/index.html.md @@ -0,0 +1,189 @@ +--- +title: API Reference + +language_tabs: + - shell + - ruby + - python + - javascript + +toc_footers: + - Sign Up for a Developer Key + - Documentation Powered by Slate + +includes: + - errors + +search: true +--- + +# Introduction + +Welcome to the Kittn API! You can use our API to access Kittn API endpoints, which can get information on various cats, kittens, and breeds in our database. + +We have language bindings in Shell, Ruby, and Python! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right. + +This example API documentation page was created with [Slate](https://github.com/tripit/slate). Feel free to edit it and use it as a base for your own API's documentation. + +# Authentication + +> To authorize, use this code: + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +``` + +```shell +# With shell, you can just pass the correct header with each request +curl "api_endpoint_here" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +``` + +> Make sure to replace `meowmeowmeow` with your API key. + +Kittn uses API keys to allow access to the API. You can register a new Kittn API key at our [developer portal](http://example.com/developers). + +Kittn expects for the API key to be included in all API requests to the server in a header that looks like the following: + +`Authorization: meowmeowmeow` + + + +# Kittens + +## Get All Kittens + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +api.kittens.get +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +api.kittens.get() +``` + +```shell +curl "http://example.com/api/kittens" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +let kittens = api.kittens.get(); +``` + +> The above command returns JSON structured like this: + +```json +[ + { + "id": 1, + "name": "Fluffums", + "breed": "calico", + "fluffiness": 6, + "cuteness": 7 + }, + { + "id": 2, + "name": "Max", + "breed": "unknown", + "fluffiness": 5, + "cuteness": 10 + } +] +``` + +This endpoint retrieves all kittens. + +### HTTP Request + +`GET http://example.com/api/kittens` + +### Query Parameters + +Parameter | Default | Description +--------- | ------- | ----------- +include_cats | false | If set to true, the result will also include cats. +available | true | If set to false, the result will include kittens that have already been adopted. + + + +## Get a Specific Kitten + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +api.kittens.get(2) +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +api.kittens.get(2) +``` + +```shell +curl "http://example.com/api/kittens/2" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +let max = api.kittens.get(2); +``` + +> The above command returns JSON structured like this: + +```json +{ + "id": 2, + "name": "Max", + "breed": "unknown", + "fluffiness": 5, + "cuteness": 10 +} +``` + +This endpoint retrieves a specific kitten. + + + +### HTTP Request + +`GET http://example.com/kittens/` + +### URL Parameters + +Parameter | Description +--------- | ----------- +ID | The ID of the kitten to retrieve + diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js index 1a124bb68..992180b76 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js @@ -1,3 +1,5 @@ +//= require ../lib/_jquery + /* Copyright 2008-2013 Concur Technologies, Inc. @@ -28,9 +30,11 @@ under the License. $(".lang-selector a").removeClass('active'); $(".lang-selector a[data-language-name='" + language + "']").addClass('active'); for (var i=0; i < languages.length; i++) { - $(".highlight." + languages[i]).hide(); + $(".highlight.tab-" + languages[i]).hide(); + $(".lang-specific." + languages[i]).hide(); } - $(".highlight." + language).show(); + $(".highlight.tab-" + language).show(); + $(".lang-specific." + language).show(); global.toc.calculateHeights(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js index 91f38a04e..5ace538d8 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js @@ -1,4 +1,5 @@ //= require ../lib/_lunr +//= require ../lib/_jquery //= require ../lib/_jquery.highlight (function () { 'use strict'; diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js index bc2aa3e1f..21d080006 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js @@ -1,3 +1,4 @@ +//= require ../lib/_jquery //= require ../lib/_jquery_ui //= require ../lib/_jquery.tocify //= require ../lib/_imagesloaded.min @@ -47,6 +48,7 @@ $(function() { makeToc(); animate(); + setupLanguages($('body').data('languages')); $('.content').imagesLoaded( function() { global.toc.calculateHeights(); }); diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js new file mode 100644 index 000000000..b78120e6a --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js @@ -0,0 +1,9831 @@ +/*! + * jQuery JavaScript Library v2.2.0 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-01-08T20:02Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +//"use strict"; +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "2.2.0", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; + }, + + isPlainObject: function( obj ) { + + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { + return false; + } + + // If the function hasn't returned already, we're confident that + // |obj| is a plain object, created by {} or constructed with new Object + return true; + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.2.1 + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-17 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var m = context.getElementById( id ); + return m ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + docElem.appendChild( div ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( function() { + + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // Add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ); + } else { + --remaining; + } + } + } + + // If we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +} ); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data, camelKey; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
            " ], + col: [ 2, "", "
            " ], + tr: [ 2, "", "
            " ], + td: [ 3, "", "
            " ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +function manipulationTarget( elem, content ) { + if ( jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return elem.getElementsByTagName( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + + // Keep domManip exposed until 3.0 (gh-2225) + domManip: domManip, + + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); + + +var iframe, + elemdisplay = { + + // Support: Firefox + // We have to pre-define these values for FF (#10227) + HTML: "block", + BODY: "block" + }; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ + +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + display = jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = ( iframe || jQuery( "